├── .DS_Store ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CODE_OF_CONDUCT.md ├── Example ├── .DS_Store └── HypeUI-Example │ ├── .DS_Store │ ├── HypeUI-Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ └── HypeUI-Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── SceneDelegate.swift │ └── ViewController.swift ├── HypeUI.podspec ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── .DS_Store └── HypeUI │ ├── Alignment.swift │ ├── AlignmentView.swift │ ├── Behavior.swift │ ├── Button.swift │ ├── DynamicLinkable.swift │ ├── Edge.swift │ ├── Gradient.swift │ ├── GradientView.swift │ ├── HStack.swift │ ├── Image.swift │ ├── LinearGradient.swift │ ├── NSObject+Retain.swift │ ├── PassthroughStackView.swift │ ├── PassthroughView.swift │ ├── ScrollView.swift │ ├── Spacer.swift │ ├── Text.swift │ ├── UIView+Passthrough.swift │ ├── UIView+Search.swift │ ├── UIView+ViewBuildable.swift │ ├── UnitPoint.swift │ ├── VStack.swift │ ├── View.swift │ ├── ViewArrayBuilder.swift │ ├── ViewBuildable.swift │ └── ZStack.swift └── Tests └── HypeUITests ├── AlignmentTests.swift ├── HStackTests.swift ├── ScrollViewTests.swift ├── SpacerTests.swift ├── TextTests.swift ├── UIView+PassthroughTests.swift ├── ViewArrayBuilderTests.swift ├── ViewBuildableTests.swift ├── ViewTests.swift ├── XCLayoutTestCase.swift └── ZStackTests.swift /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperconnect/HypeUI/8e1cd9a00cd615eb5283d29fcf656ebf1022d1ee/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## AppCode project setting 26 | .idea/ 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | 54 | # CocoaPods 55 | # 56 | # We recommend against adding the Pods directory to your .gitignore. However 57 | # you should judge for yourself, the pros and cons are mentioned at: 58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 59 | # 60 | # Pods/ 61 | # 62 | # Add this line if you want to avoid checking in source code from the Xcode workspace 63 | # *.xcworkspace 64 | 65 | # Carthage 66 | # 67 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 68 | # Carthage/Checkouts 69 | 70 | Carthage/Build/ 71 | 72 | # Accio dependency management 73 | Dependencies/ 74 | .accio/ 75 | 76 | # fastlane 77 | # 78 | # It is recommended to not store the screenshots in the git repo. 79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 80 | # For more information about the recommended setup visit: 81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 82 | 83 | fastlane/report.xml 84 | fastlane/Preview.html 85 | fastlane/screenshots/**/*.png 86 | fastlane/test_output 87 | 88 | # Code Injection 89 | # 90 | # After new code Injection tools there's a generated folder /iOSInjectionProject 91 | # https://github.com/johnno1962/injectionforxcode 92 | 93 | iOSInjectionProject/ 94 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | @hyperconnect. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Example/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperconnect/HypeUI/8e1cd9a00cd615eb5283d29fcf656ebf1022d1ee/Example/.DS_Store -------------------------------------------------------------------------------- /Example/HypeUI-Example/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperconnect/HypeUI/8e1cd9a00cd615eb5283d29fcf656ebf1022d1ee/Example/HypeUI-Example/.DS_Store -------------------------------------------------------------------------------- /Example/HypeUI-Example/HypeUI-Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FC8A9F09291D33C100022ED3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC8A9F08291D33C100022ED3 /* AppDelegate.swift */; }; 11 | FC8A9F0B291D33C100022ED3 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC8A9F0A291D33C100022ED3 /* SceneDelegate.swift */; }; 12 | FC8A9F0D291D33C100022ED3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC8A9F0C291D33C100022ED3 /* ViewController.swift */; }; 13 | FC8A9F10291D33C100022ED3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FC8A9F0E291D33C100022ED3 /* Main.storyboard */; }; 14 | FC8A9F12291D33C200022ED3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FC8A9F11291D33C200022ED3 /* Assets.xcassets */; }; 15 | FC8A9F15291D33C200022ED3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FC8A9F13291D33C200022ED3 /* LaunchScreen.storyboard */; }; 16 | FC8A9F1E291D33FC00022ED3 /* HypeUI in Frameworks */ = {isa = PBXBuildFile; productRef = FC8A9F1D291D33FC00022ED3 /* HypeUI */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | FC8A9F05291D33C100022ED3 /* HypeUI-Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "HypeUI-Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | FC8A9F08291D33C100022ED3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | FC8A9F0A291D33C100022ED3 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 23 | FC8A9F0C291D33C100022ED3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 24 | FC8A9F0F291D33C100022ED3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 25 | FC8A9F11291D33C200022ED3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | FC8A9F14291D33C200022ED3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 27 | FC8A9F16291D33C300022ED3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 28 | /* End PBXFileReference section */ 29 | 30 | /* Begin PBXFrameworksBuildPhase section */ 31 | FC8A9F02291D33C100022ED3 /* Frameworks */ = { 32 | isa = PBXFrameworksBuildPhase; 33 | buildActionMask = 2147483647; 34 | files = ( 35 | FC8A9F1E291D33FC00022ED3 /* HypeUI in Frameworks */, 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | FC8A9EFC291D33C100022ED3 = { 43 | isa = PBXGroup; 44 | children = ( 45 | FC8A9F07291D33C100022ED3 /* HypeUI-Example */, 46 | FC8A9F06291D33C100022ED3 /* Products */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | FC8A9F06291D33C100022ED3 /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | FC8A9F05291D33C100022ED3 /* HypeUI-Example.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | FC8A9F07291D33C100022ED3 /* HypeUI-Example */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | FC8A9F08291D33C100022ED3 /* AppDelegate.swift */, 62 | FC8A9F0A291D33C100022ED3 /* SceneDelegate.swift */, 63 | FC8A9F0C291D33C100022ED3 /* ViewController.swift */, 64 | FC8A9F0E291D33C100022ED3 /* Main.storyboard */, 65 | FC8A9F11291D33C200022ED3 /* Assets.xcassets */, 66 | FC8A9F13291D33C200022ED3 /* LaunchScreen.storyboard */, 67 | FC8A9F16291D33C300022ED3 /* Info.plist */, 68 | ); 69 | path = "HypeUI-Example"; 70 | sourceTree = ""; 71 | }; 72 | /* End PBXGroup section */ 73 | 74 | /* Begin PBXNativeTarget section */ 75 | FC8A9F04291D33C100022ED3 /* HypeUI-Example */ = { 76 | isa = PBXNativeTarget; 77 | buildConfigurationList = FC8A9F19291D33C300022ED3 /* Build configuration list for PBXNativeTarget "HypeUI-Example" */; 78 | buildPhases = ( 79 | FC8A9F01291D33C100022ED3 /* Sources */, 80 | FC8A9F02291D33C100022ED3 /* Frameworks */, 81 | FC8A9F03291D33C100022ED3 /* Resources */, 82 | ); 83 | buildRules = ( 84 | ); 85 | dependencies = ( 86 | ); 87 | name = "HypeUI-Example"; 88 | packageProductDependencies = ( 89 | FC8A9F1D291D33FC00022ED3 /* HypeUI */, 90 | ); 91 | productName = "HypeUI-Example"; 92 | productReference = FC8A9F05291D33C100022ED3 /* HypeUI-Example.app */; 93 | productType = "com.apple.product-type.application"; 94 | }; 95 | /* End PBXNativeTarget section */ 96 | 97 | /* Begin PBXProject section */ 98 | FC8A9EFD291D33C100022ED3 /* Project object */ = { 99 | isa = PBXProject; 100 | attributes = { 101 | BuildIndependentTargetsInParallel = 1; 102 | LastSwiftUpdateCheck = 1410; 103 | LastUpgradeCheck = 1410; 104 | TargetAttributes = { 105 | FC8A9F04291D33C100022ED3 = { 106 | CreatedOnToolsVersion = 14.1; 107 | }; 108 | }; 109 | }; 110 | buildConfigurationList = FC8A9F00291D33C100022ED3 /* Build configuration list for PBXProject "HypeUI-Example" */; 111 | compatibilityVersion = "Xcode 14.0"; 112 | developmentRegion = en; 113 | hasScannedForEncodings = 0; 114 | knownRegions = ( 115 | en, 116 | Base, 117 | ); 118 | mainGroup = FC8A9EFC291D33C100022ED3; 119 | packageReferences = ( 120 | FC8A9F1C291D33FC00022ED3 /* XCRemoteSwiftPackageReference "HypeUI" */, 121 | ); 122 | productRefGroup = FC8A9F06291D33C100022ED3 /* Products */; 123 | projectDirPath = ""; 124 | projectRoot = ""; 125 | targets = ( 126 | FC8A9F04291D33C100022ED3 /* HypeUI-Example */, 127 | ); 128 | }; 129 | /* End PBXProject section */ 130 | 131 | /* Begin PBXResourcesBuildPhase section */ 132 | FC8A9F03291D33C100022ED3 /* Resources */ = { 133 | isa = PBXResourcesBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | FC8A9F15291D33C200022ED3 /* LaunchScreen.storyboard in Resources */, 137 | FC8A9F12291D33C200022ED3 /* Assets.xcassets in Resources */, 138 | FC8A9F10291D33C100022ED3 /* Main.storyboard in Resources */, 139 | ); 140 | runOnlyForDeploymentPostprocessing = 0; 141 | }; 142 | /* End PBXResourcesBuildPhase section */ 143 | 144 | /* Begin PBXSourcesBuildPhase section */ 145 | FC8A9F01291D33C100022ED3 /* Sources */ = { 146 | isa = PBXSourcesBuildPhase; 147 | buildActionMask = 2147483647; 148 | files = ( 149 | FC8A9F0D291D33C100022ED3 /* ViewController.swift in Sources */, 150 | FC8A9F09291D33C100022ED3 /* AppDelegate.swift in Sources */, 151 | FC8A9F0B291D33C100022ED3 /* SceneDelegate.swift in Sources */, 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | /* End PBXSourcesBuildPhase section */ 156 | 157 | /* Begin PBXVariantGroup section */ 158 | FC8A9F0E291D33C100022ED3 /* Main.storyboard */ = { 159 | isa = PBXVariantGroup; 160 | children = ( 161 | FC8A9F0F291D33C100022ED3 /* Base */, 162 | ); 163 | name = Main.storyboard; 164 | sourceTree = ""; 165 | }; 166 | FC8A9F13291D33C200022ED3 /* LaunchScreen.storyboard */ = { 167 | isa = PBXVariantGroup; 168 | children = ( 169 | FC8A9F14291D33C200022ED3 /* Base */, 170 | ); 171 | name = LaunchScreen.storyboard; 172 | sourceTree = ""; 173 | }; 174 | /* End PBXVariantGroup section */ 175 | 176 | /* Begin XCBuildConfiguration section */ 177 | FC8A9F17291D33C300022ED3 /* Debug */ = { 178 | isa = XCBuildConfiguration; 179 | buildSettings = { 180 | ALWAYS_SEARCH_USER_PATHS = NO; 181 | CLANG_ANALYZER_NONNULL = YES; 182 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 183 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 184 | CLANG_ENABLE_MODULES = YES; 185 | CLANG_ENABLE_OBJC_ARC = YES; 186 | CLANG_ENABLE_OBJC_WEAK = YES; 187 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 188 | CLANG_WARN_BOOL_CONVERSION = YES; 189 | CLANG_WARN_COMMA = YES; 190 | CLANG_WARN_CONSTANT_CONVERSION = YES; 191 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 192 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 193 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 194 | CLANG_WARN_EMPTY_BODY = YES; 195 | CLANG_WARN_ENUM_CONVERSION = YES; 196 | CLANG_WARN_INFINITE_RECURSION = YES; 197 | CLANG_WARN_INT_CONVERSION = YES; 198 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 199 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 200 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 201 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 202 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 203 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 204 | CLANG_WARN_STRICT_PROTOTYPES = YES; 205 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 206 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 207 | CLANG_WARN_UNREACHABLE_CODE = YES; 208 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 209 | COPY_PHASE_STRIP = NO; 210 | DEBUG_INFORMATION_FORMAT = dwarf; 211 | ENABLE_STRICT_OBJC_MSGSEND = YES; 212 | ENABLE_TESTABILITY = YES; 213 | GCC_C_LANGUAGE_STANDARD = gnu11; 214 | GCC_DYNAMIC_NO_PIC = NO; 215 | GCC_NO_COMMON_BLOCKS = YES; 216 | GCC_OPTIMIZATION_LEVEL = 0; 217 | GCC_PREPROCESSOR_DEFINITIONS = ( 218 | "DEBUG=1", 219 | "$(inherited)", 220 | ); 221 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 222 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 223 | GCC_WARN_UNDECLARED_SELECTOR = YES; 224 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 225 | GCC_WARN_UNUSED_FUNCTION = YES; 226 | GCC_WARN_UNUSED_VARIABLE = YES; 227 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 228 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 229 | MTL_FAST_MATH = YES; 230 | ONLY_ACTIVE_ARCH = YES; 231 | SDKROOT = iphoneos; 232 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 233 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 234 | }; 235 | name = Debug; 236 | }; 237 | FC8A9F18291D33C300022ED3 /* Release */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | ALWAYS_SEARCH_USER_PATHS = NO; 241 | CLANG_ANALYZER_NONNULL = YES; 242 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 243 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 244 | CLANG_ENABLE_MODULES = YES; 245 | CLANG_ENABLE_OBJC_ARC = YES; 246 | CLANG_ENABLE_OBJC_WEAK = YES; 247 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 248 | CLANG_WARN_BOOL_CONVERSION = YES; 249 | CLANG_WARN_COMMA = YES; 250 | CLANG_WARN_CONSTANT_CONVERSION = YES; 251 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 252 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 253 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 254 | CLANG_WARN_EMPTY_BODY = YES; 255 | CLANG_WARN_ENUM_CONVERSION = YES; 256 | CLANG_WARN_INFINITE_RECURSION = YES; 257 | CLANG_WARN_INT_CONVERSION = YES; 258 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 259 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 260 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 261 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 262 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 263 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 264 | CLANG_WARN_STRICT_PROTOTYPES = YES; 265 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 266 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | COPY_PHASE_STRIP = NO; 270 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 271 | ENABLE_NS_ASSERTIONS = NO; 272 | ENABLE_STRICT_OBJC_MSGSEND = YES; 273 | GCC_C_LANGUAGE_STANDARD = gnu11; 274 | GCC_NO_COMMON_BLOCKS = YES; 275 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 276 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 277 | GCC_WARN_UNDECLARED_SELECTOR = YES; 278 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 279 | GCC_WARN_UNUSED_FUNCTION = YES; 280 | GCC_WARN_UNUSED_VARIABLE = YES; 281 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 282 | MTL_ENABLE_DEBUG_INFO = NO; 283 | MTL_FAST_MATH = YES; 284 | SDKROOT = iphoneos; 285 | SWIFT_COMPILATION_MODE = wholemodule; 286 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 287 | VALIDATE_PRODUCT = YES; 288 | }; 289 | name = Release; 290 | }; 291 | FC8A9F1A291D33C300022ED3 /* Debug */ = { 292 | isa = XCBuildConfiguration; 293 | buildSettings = { 294 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 295 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 296 | CODE_SIGN_STYLE = Automatic; 297 | CURRENT_PROJECT_VERSION = 1; 298 | GENERATE_INFOPLIST_FILE = YES; 299 | INFOPLIST_FILE = "HypeUI-Example/Info.plist"; 300 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 301 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 302 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 303 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 304 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 305 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 306 | LD_RUNPATH_SEARCH_PATHS = ( 307 | "$(inherited)", 308 | "@executable_path/Frameworks", 309 | ); 310 | MARKETING_VERSION = 1.0; 311 | PRODUCT_BUNDLE_IDENTIFIER = "com.hpcnt.hypeui-example.HypeUI-Example"; 312 | PRODUCT_NAME = "$(TARGET_NAME)"; 313 | SWIFT_EMIT_LOC_STRINGS = YES; 314 | SWIFT_VERSION = 5.0; 315 | TARGETED_DEVICE_FAMILY = "1,2"; 316 | }; 317 | name = Debug; 318 | }; 319 | FC8A9F1B291D33C300022ED3 /* Release */ = { 320 | isa = XCBuildConfiguration; 321 | buildSettings = { 322 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 323 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 324 | CODE_SIGN_STYLE = Automatic; 325 | CURRENT_PROJECT_VERSION = 1; 326 | GENERATE_INFOPLIST_FILE = YES; 327 | INFOPLIST_FILE = "HypeUI-Example/Info.plist"; 328 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 329 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 330 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 331 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 332 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 333 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 334 | LD_RUNPATH_SEARCH_PATHS = ( 335 | "$(inherited)", 336 | "@executable_path/Frameworks", 337 | ); 338 | MARKETING_VERSION = 1.0; 339 | PRODUCT_BUNDLE_IDENTIFIER = "com.hpcnt.hypeui-example.HypeUI-Example"; 340 | PRODUCT_NAME = "$(TARGET_NAME)"; 341 | SWIFT_EMIT_LOC_STRINGS = YES; 342 | SWIFT_VERSION = 5.0; 343 | TARGETED_DEVICE_FAMILY = "1,2"; 344 | }; 345 | name = Release; 346 | }; 347 | /* End XCBuildConfiguration section */ 348 | 349 | /* Begin XCConfigurationList section */ 350 | FC8A9F00291D33C100022ED3 /* Build configuration list for PBXProject "HypeUI-Example" */ = { 351 | isa = XCConfigurationList; 352 | buildConfigurations = ( 353 | FC8A9F17291D33C300022ED3 /* Debug */, 354 | FC8A9F18291D33C300022ED3 /* Release */, 355 | ); 356 | defaultConfigurationIsVisible = 0; 357 | defaultConfigurationName = Release; 358 | }; 359 | FC8A9F19291D33C300022ED3 /* Build configuration list for PBXNativeTarget "HypeUI-Example" */ = { 360 | isa = XCConfigurationList; 361 | buildConfigurations = ( 362 | FC8A9F1A291D33C300022ED3 /* Debug */, 363 | FC8A9F1B291D33C300022ED3 /* Release */, 364 | ); 365 | defaultConfigurationIsVisible = 0; 366 | defaultConfigurationName = Release; 367 | }; 368 | /* End XCConfigurationList section */ 369 | 370 | /* Begin XCRemoteSwiftPackageReference section */ 371 | FC8A9F1C291D33FC00022ED3 /* XCRemoteSwiftPackageReference "HypeUI" */ = { 372 | isa = XCRemoteSwiftPackageReference; 373 | repositoryURL = "https://github.com/hyperconnect/HypeUI"; 374 | requirement = { 375 | kind = upToNextMajorVersion; 376 | minimumVersion = 0.1.0; 377 | }; 378 | }; 379 | /* End XCRemoteSwiftPackageReference section */ 380 | 381 | /* Begin XCSwiftPackageProductDependency section */ 382 | FC8A9F1D291D33FC00022ED3 /* HypeUI */ = { 383 | isa = XCSwiftPackageProductDependency; 384 | package = FC8A9F1C291D33FC00022ED3 /* XCRemoteSwiftPackageReference "HypeUI" */; 385 | productName = HypeUI; 386 | }; 387 | /* End XCSwiftPackageProductDependency section */ 388 | }; 389 | rootObject = FC8A9EFD291D33C100022ED3 /* Project object */; 390 | } 391 | -------------------------------------------------------------------------------- /Example/HypeUI-Example/HypeUI-Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/HypeUI-Example/HypeUI-Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/HypeUI-Example/HypeUI-Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "hypeui", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/hyperconnect/HypeUI", 7 | "state" : { 8 | "revision" : "f485ca4ffd96df345c5ecf6864d0ca8a172dea00", 9 | "version" : "0.3.0" 10 | } 11 | }, 12 | { 13 | "identity" : "rxswift", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/ReactiveX/RxSwift.git", 16 | "state" : { 17 | "revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8", 18 | "version" : "6.5.0" 19 | } 20 | }, 21 | { 22 | "identity" : "snapkit", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/SnapKit/SnapKit.git", 25 | "state" : { 26 | "revision" : "f222cbdf325885926566172f6f5f06af95473158", 27 | "version" : "5.6.0" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Example/HypeUI-Example/HypeUI-Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | @main 20 | class AppDelegate: UIResponder, UIApplicationDelegate { 21 | 22 | 23 | 24 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 25 | // Override point for customization after application launch. 26 | return true 27 | } 28 | 29 | // MARK: UISceneSession Lifecycle 30 | 31 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 32 | // Called when a new scene session is being created. 33 | // Use this method to select a configuration to create the new scene with. 34 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 35 | } 36 | 37 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 38 | // Called when the user discards a scene session. 39 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 40 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Example/HypeUI-Example/HypeUI-Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/HypeUI-Example/HypeUI-Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example/HypeUI-Example/HypeUI-Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/HypeUI-Example/HypeUI-Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/HypeUI-Example/HypeUI-Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/HypeUI-Example/HypeUI-Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/HypeUI-Example/HypeUI-Example/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 20 | 21 | var window: UIWindow? 22 | 23 | 24 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 25 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 26 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 27 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 28 | guard let _ = (scene as? UIWindowScene) else { return } 29 | } 30 | 31 | func sceneDidDisconnect(_ scene: UIScene) { 32 | // Called as the scene is being released by the system. 33 | // This occurs shortly after the scene enters the background, or when its session is discarded. 34 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 35 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 36 | } 37 | 38 | func sceneDidBecomeActive(_ scene: UIScene) { 39 | // Called when the scene has moved from an inactive state to an active state. 40 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 41 | } 42 | 43 | func sceneWillResignActive(_ scene: UIScene) { 44 | // Called when the scene will move from an active state to an inactive state. 45 | // This may occur due to temporary interruptions (ex. an incoming phone call). 46 | } 47 | 48 | func sceneWillEnterForeground(_ scene: UIScene) { 49 | // Called as the scene transitions from the background to the foreground. 50 | // Use this method to undo the changes made on entering the background. 51 | } 52 | 53 | func sceneDidEnterBackground(_ scene: UIScene) { 54 | // Called as the scene transitions from the foreground to the background. 55 | // Use this method to save data, release shared resources, and store enough scene-specific state information 56 | // to restore the scene back to its current state. 57 | } 58 | 59 | 60 | } 61 | 62 | -------------------------------------------------------------------------------- /Example/HypeUI-Example/HypeUI-Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import HypeUI 19 | 20 | class ViewController: UIViewController { 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | 25 | view.addSubviewWithFit( 26 | ScrollView(.vertical, showsIndicators: false) { 27 | VStack(spacing: 10) { 28 | Text("Text") 29 | .font(UIFont.systemFont(ofSize: 32, weight: .heavy)) 30 | Spacer() 31 | .background(.systemGray5) 32 | .frame(height: 1) 33 | Text("Heading 1") 34 | .font(UIFont.systemFont(ofSize: 32, weight: .bold)) 35 | .foregroundColor(UIColor.black) 36 | Text("Heading 2") 37 | .font(UIFont.systemFont(ofSize: 24, weight: .bold)) 38 | Text("Heading 3") 39 | .font(UIFont.systemFont(ofSize: 16, weight: .bold)) 40 | Text("Heavy") 41 | .font(UIFont.systemFont(ofSize: 16, weight: .heavy)) 42 | Text("Bold") 43 | .font(UIFont.systemFont(ofSize: 16, weight: .bold)) 44 | Text("Regular") 45 | .font(UIFont.systemFont(ofSize: 16, weight: .regular)) 46 | Text("Light") 47 | .font(UIFont.systemFont(ofSize: 16, weight: .light)) 48 | Text("Alignement Center") 49 | .font(UIFont.systemFont(ofSize: 16, weight: .light)) 50 | .textAligned(.center) 51 | Text("🌺 HypeUI is a implementation of Apple's SwiftUI DSL style based on UIKit") 52 | .font(UIFont.systemFont(ofSize: 16, weight: .regular)) 53 | .foregroundColor(UIColor.systemGray) 54 | .lineLimit(3) 55 | Spacer() 56 | .frame(height: 64) 57 | Text("Button") 58 | .font(UIFont.systemFont(ofSize: 32, weight: .heavy)) 59 | Spacer() 60 | .background(.systemGray5) 61 | .frame(height: 1) 62 | Button(action: { print("🐠 Click Me!!") }) { 63 | Text("🐠 Click Me!!") 64 | .font(UIFont.systemFont(ofSize: 16, weight: .bold )) 65 | .foregroundColor(UIColor.white) 66 | .textAligned(.center) 67 | .padding(.horizontal, 24) 68 | .frame(height: 48) 69 | .background(.systemYellow) 70 | .cornerRadius(24) 71 | }.padding(.horizontal, 64) 72 | Button(action: { print("Border Button") }) { 73 | Text("Border Button") 74 | .font(UIFont.systemFont(ofSize: 16, weight: .bold )) 75 | .foregroundColor(UIColor.black) 76 | .textAligned(.center) 77 | .padding(.horizontal, 24) 78 | .frame(height: 48) 79 | .background(UIColor.white) 80 | .border(UIColor.black, width: 2) 81 | .cornerRadius(24) 82 | }.padding(.horizontal, 64) 83 | Button(action: { print("Stack Button")}) { 84 | ZStack { 85 | UIView() 86 | .background(.systemBlue) 87 | .frame(height: 48) 88 | .cornerRadius(24) 89 | HStack(alignment: .center) { 90 | VStack(alignment: .center, spacing: 1) { 91 | Text("Stack Button") 92 | .font(UIFont.systemFont(ofSize: 16, weight: .bold)) 93 | .foregroundColor(UIColor.white) 94 | HStack(alignment: .center, spacing: 2) { 95 | Text("🌎") 96 | .font(UIFont.systemFont(ofSize: 12, weight: .medium)) 97 | Text("Earth") 98 | .font(UIFont.systemFont(ofSize: 14, weight: .medium)) 99 | .foregroundColor(UIColor.white) 100 | } 101 | } 102 | } 103 | } 104 | }.padding(.horizontal, 64) 105 | Spacer() 106 | .frame(height: 64) 107 | Text("ScrollView") 108 | .font(UIFont.systemFont(ofSize: 32, weight: .heavy)) 109 | Spacer() 110 | .background(.systemGray5) 111 | .frame(height: 1) 112 | ScrollView(.horizontal, showsIndicators: false) { 113 | HStack(alignment: .center, spacing: 8) { 114 | ["Proactive", "One Team", "Aim High", "Priortize", "Move Fast", "Logical", "Open"].map { 115 | Text($0) 116 | .font(UIFont.systemFont(ofSize: 18, weight: .bold)) 117 | .foregroundColor(UIColor.black) 118 | .textAligned(.center) 119 | .background(.systemGray6) 120 | .frame(width: 140, height: 64) 121 | .cornerRadius(32) 122 | } 123 | } 124 | } 125 | Spacer() 126 | .frame(height: 64) 127 | Text("View Modifier") 128 | .font(UIFont.systemFont(ofSize: 32, weight: .heavy)) 129 | Spacer() 130 | .background(.systemGray5) 131 | .frame(height: 1) 132 | Text("Padding") 133 | .foregroundColor(UIColor.white) 134 | .textAligned(.center) 135 | .font(UIFont.systemFont(ofSize: 14, weight: .bold)) 136 | .padding(.all, 12) 137 | .background(UIColor.systemRed) 138 | .padding(.all, 12) 139 | .background(UIColor.systemOrange) 140 | .padding(.all, 12) 141 | .background(UIColor.systemYellow) 142 | .padding(.all, 12) 143 | .background(UIColor.systemGreen) 144 | .padding(.all, 12) 145 | .background(UIColor.systemBlue) 146 | .padding(.all, 12) 147 | .background(UIColor.systemIndigo) 148 | .padding(.all, 12) 149 | .background(UIColor.systemPurple) 150 | Text("Border") 151 | .foregroundColor(UIColor.white) 152 | .textAligned(.center) 153 | .font(UIFont.systemFont(ofSize: 14, weight: .bold)) 154 | .padding(.all, 12) 155 | .background(UIColor.systemGray) 156 | .border(UIColor.black, width: 2) 157 | } 158 | .padding(.all, 24) 159 | } 160 | ) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /HypeUI.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "HypeUI" 3 | s.version = "0.3.0" 4 | s.summary = "HypeUI is a implementation of Apple's SwiftUI DSL style based on UIKit" 5 | s.homepage = "https://github.com/hyperconnect/HypeUI" 6 | s.license = { :type => 'Apache 2.0', :file => "LICENSE" } 7 | s.author = { 'Cruz' => 'cruz@hpcnt.com', 'Xeon' => 'xeon@hpcnt.com' } 8 | s.source = { :git => 'https://github.com/hyperconnect/HypeUI.git', :tag => s.version.to_s } 9 | s.ios.deployment_target = '12.0' 10 | s.source_files = 'Sources/**/*.swift' 11 | s.swift_version = '5.0' 12 | s.dependency "RxSwift", '~> 6.0' 13 | s.dependency "RxCocoa", '~> 6.0' 14 | s.dependency "SnapKit", '~> 5.0.0' 15 | end 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "RxSwift", 6 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "b4307ba0b6425c0ba4178e138799946c3da594f8", 10 | "version": "6.5.0" 11 | } 12 | }, 13 | { 14 | "package": "SnapKit", 15 | "repositoryURL": "https://github.com/SnapKit/SnapKit.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "f222cbdf325885926566172f6f5f06af95473158", 19 | "version": "5.6.0" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | 27 | -------------------------------------------------------------------------------- /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 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "HypeUI", 7 | platforms: [ 8 | .iOS(.v12) 9 | ], 10 | products: [ 11 | .library(name: "HypeUI", targets: ["HypeUI"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "6.2.0")), 15 | .package(url: "https://github.com/SnapKit/SnapKit.git", .upToNextMajor(from: "5.6.0")), 16 | ], 17 | targets: [ 18 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 19 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 20 | .target( 21 | name: "HypeUI", 22 | dependencies: ["RxSwift", "RxCocoa", "SnapKit"]), 23 | .testTarget( 24 | name: "HypeUITests", 25 | dependencies: ["HypeUI"]), 26 | ] 27 | ) 28 | 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HypeUI 2 | 🚀 HypeUI is a implementation of Apple's SwiftUI DSL style based on UIKit 3 | Want to enjoy SwiftUI syntax with UIKit? It's time to use HypeUI 😊 4 | 5 | [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome) 6 | ![Swift](https://img.shields.io/badge/Swift-5.0-orange.svg) 7 | 8 | | | Why to use HypeUI? | 9 | ---|----------------- 10 | 📱 | Support iOS 12+ 11 | ✨ | HypeUI is compatible with UIKit based project using SwiftUI style syntax 12 | 🏄‍♂️ | Reduce UI & Autolayout codes more than 30% 13 | ⛳️ | Provide UI binding extension with RxSwift and RxCocoa 14 | 🦄 | Easy to use! 15 | ✈️ | Improve readability and intuitiveness of complex layouts 16 | 🤩 | Have a blast 17 | ⛷ | Customize reusable component, design system 18 | ❄️ | Test with accessibility Identifier 19 | 20 | ## Contents 21 | 22 | - [Requirements](#requirements) 23 | - [Installation](#installation) 24 | - [Supported Features](#supported_features) 25 | * [View Modifier](#view_modifier) 26 | * [Text Modifier](#text_modifier) 27 | * [Stack Modifier](#stack_modifier) 28 | * [ScrollView Modifier](#scrollview_modifier) 29 | - [Usage](#usage) 30 | * [HStack](#hstack) 31 | * [VStack](#vstack) 32 | * [ZStack](#zstack) 33 | * [Button](#button) 34 | * [Text](#text) 35 | * [Image](#image) 36 | * [ScrollView](#scrollview) 37 | * [Behavior](#behavior) 38 | * [Spacer](#spacer) 39 | * [LinearGradient](#lineargradient) 40 | * [ViewBuildable](#viewbuildable) 41 | - [Dependencies](#dependencies) 42 | 43 | ## Requirements 44 | 45 | - iOS 12.0+ 46 | - XCode 13.0+ 47 | - Swift 5.0+ 48 | 49 | ## Installation 50 | 51 | ### Swift Package Manager 52 | 53 | [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies. 54 | 55 | > Xcode 13+ is required to build HypeUI using Swift Package Manager. 56 | 57 | To integrate HypeUI into your Xcode project using Swift Package Manager, add it to the dependencies value of your `Package.swift`: 58 | 59 | ```swift 60 | dependencies: [ 61 | .package(url: "https://github.com/hyperconnect/HypeUI", .upToNextMajor(from: "0.3.0")) 62 | ] 63 | ``` 64 | 65 | ### CocoaPod 66 | 67 | The preferred installation method is with [CocoaPods](https://cocoapods.org). Add the following to your `Podfile`: 68 | 69 | ```ruby 70 | pod 'HypeUI' 71 | ``` 72 | 73 | ## Supported Features 74 | 75 | | | Supported Features | 76 | ----------------|---------------- 77 | HStack | ✅ 78 | VStack | ✅ 79 | ZStack | ✅ 80 | Button | ✅ 81 | Text | ✅ 82 | Image | ✅ 83 | ScrollView | ✅ 84 | Behavior | ✅ 85 | Spacer | ✅ 86 | LinearGradient | ✅ 87 | AlignmentView | ✅ 88 | ViewBuildable | ✅ 89 | View Modifier | ✅ 90 | Text Modifier | ✅ 91 | Stack Modifier | ✅ 92 | ScrollView Modifier | ✅ 93 | 94 | 95 | ### View Modifier 96 | 97 | | name | Description | 98 | ---------------------------------|---------------- 99 | setHContentHugging | Adjusts the priority for a view to resist growing beyond its intrinsic size horizontally. 100 | setVContentHugging | Adjusts the priority for a view to resist growing beyond its intrinsic size vertically. 101 | setHContentCompressionResistance | Adjusts the priority for a view to resist shrinking below its intrinsic size horizontally. 102 | setVContentCompressionResistance | Adjusts the priority for a view to resist shrinking below its intrinsic size vertically. 103 | makeRatio | Sets the aspect ratio constraint for the view's size. 104 | cornerRadius | Applies a corner radius to the view to create rounded corners. 105 | border | Adds a border with specified color and width to the view. 106 | background | Sets the background color of the view. 107 | makeContentMode | Sets the content mode of the view. 108 | frame | Positions the view within a specified frame size. 109 | padding | Adds padding around specific edges of the view. 110 | allowsHitTesting | Enables or disables the view's interaction with touch events. 111 | masksToBounds | Clips the view's sublayers to its boundaries. 112 | accessibilityIdentifier | Assigns an identifier used to find this view in tests. 113 | overlay | Places specified views in front of the view. 114 | background | Layers the views that you specify behind this view. 115 | center | Centers the view within a new parent view. 116 | tint | Applies a tint color to the view. 117 | opacity | Sets the transparency level of the view. 118 | scaleEffect | Scales the view by specified factors along the x and y axes. 119 | rotationEffect | Rotates the view by a specified angle around a given anchor point. 120 | 121 | 122 | ### Text Modifier 123 | 124 | | name | Description | 125 | ---------------------------------|---------------- 126 | font | Sets the font of the text. 127 | foregroundColor | Applies a foreground color to the text. 128 | textAligned | Sets the alignment of the text within its container. 129 | lineLimit | Specifies the maximum number of lines the text can span. 130 | lineBreakMode | Defines how text wraps when it reaches the edge of its container. 131 | adjustFontSize | Adjusts the font size of the text to fit its width. 132 | minimumScaleFactor | Sets the smallest multiplier for text size reduction to fit the width. 133 | preferredMaxLayoutWidth | Sets the preferred maximum width for the Text object and enables method chaining. 134 | baselineAdjusted | Applies a baseline adjustment to the Text object and enables method chaining. 135 | 136 | 137 | ### Stack Modifier 138 | 139 | | name | Description | 140 | ---------------------------------|---------------- 141 | distributed | Modify stack's distribution layout. 142 | 143 | 144 | ### ScrollView Modifier 145 | 146 | | name | Description | 147 | ---------------------------------|---------------- 148 | bounces | Modify scroll view bounces. 149 | isPagingEnabled | Modify scroll view paging enabled. 150 | isScrollEnabled | Modify scroll view enabled. 151 | 152 | ## Usage 153 | 154 | ### HStack 155 | 156 | ```swift 157 | HStack(alinement: .center, spacing: 4) { 158 | Image(Asset.icStar.image) 159 | .frame(width: 12, height: 12) 160 | Text() 161 | .foregroundColor(UIColor.black) 162 | .font(UIFont.systemFont(ofSize: 14, weight: .regular)) 163 | Spacer() 164 | } 165 | ``` 166 | 167 | ### VStack 168 | 169 | ```swift 170 | VStack(spacing: 8) { 171 | Text() 172 | .foregroundColor(UIColor.black) 173 | .font(UIFont.systemFont(ofSize: 14, weight: .regular)) 174 | Spacer() 175 | } 176 | ``` 177 | 178 | ### ZStack 179 | 180 | ```swift 181 | ZStack { 182 | HStack(alinement: .center, spacing: 4) { 183 | Image(Asset.icStar.image) 184 | .frame(width: 12, height: 12) 185 | Text() 186 | .foregroundColor(UIColor.black) 187 | .font(UIFont.systemFont(ofSize: 14, weight: .regular)) 188 | Spacer() 189 | } 190 | VStack { 191 | Text() 192 | .foregroundColor(UIColor.black) 193 | .font(UIFont.systemFont(ofSize: 14, weight: .regular)) 194 | Spacer() 195 | } 196 | } 197 | ``` 198 | 199 | ### Button 200 | 201 | ```swift 202 | Button(action: { // DO SOMETHING ex) reactor action, closure }) { 203 | HStack(alignment: .center, spacing: 5.0) { 204 | Image("cart") 205 | .padding(.leading, 10.0) 206 | Text("Add to Cart") 207 | .foregroundColor(.black) 208 | .padding(.all, 10.0) 209 | } 210 | } 211 | .background(Color.gray) 212 | .cornerRadius(5) 213 | ``` 214 | 215 | ### Text 216 | 217 | ```swift 218 | Text("✨") 219 | .foregroundColor(UIColor.black) 220 | .font(UIFont.systemFont(ofSize: 14, weight: .regular)) 221 | .textAligned(.center) 222 | .background(.white) 223 | .cornerRadius(16) 224 | ``` 225 | 226 | ### Image 227 | 228 | ```swift 229 | Image(Resource.Profile.placeholderImage) 230 | .frame(width: 48, height: 48) 231 | .cornerRadius(24) 232 | ``` 233 | 234 | ### ScrollView 235 | 236 | ```swift 237 | // MARK: Example 238 | ScrollView(.vertical, showsIndicators: false) { 239 | VStack(alignment: .fill) { 240 | Image(image: Asset.imgPopupPrivateCall.image) 241 | .makeRatio(0.46106) 242 | Spacer() 243 | .frame(height: 24) 244 | VStack { 245 | viewModel.messages.map { message in 246 | HStack(alignment: .top, spacing: 8) { 247 | Text("•") 248 | .font(UIFont.systemFont(ofSize: 14, weight: .regular)) 249 | .foregroundColor(.Palette.gray04) 250 | .frame(width: 6) 251 | Text(message) 252 | .font(UIFont.systemFont(ofSize: 14, weight: .regular)) 253 | .foregroundColor(.Palette.gray04) 254 | .lineLimit(2) 255 | .lineBreakMode(.byCharWrapping) 256 | Spacer() 257 | .frame(width: 5) 258 | } 259 | .padding(.vertical, 8) 260 | } 261 | } 262 | Spacer() 263 | .frame(height: 16) 264 | } 265 | } 266 | ``` 267 | 268 | ### @Behavior - It's seems like SwiftUI's @State using DynamicLinkable 😎 269 | 270 | ```swift 271 | @Behavior var isLive: Bool = false 272 | @Behavior var username: String? = nil 273 | @Behavior var profileImageURL: URL? = nil 274 | 275 | // MARK Example 276 | final class SearchHostHistoryViewCell: UICollectionViewCell { 277 | @Behavior var isLive: Bool = false 278 | @Behavior var username: String? = nil 279 | @Behavior var profileImageURL: URL? = nil 280 | 281 | override init(frame: CGRect) { 282 | super.init(frame: frame) 283 | contentView.addSubviewWithFit( 284 | ZStack { 285 | VStack(alignment: .center, spacing: 8) { 286 | Image(Resource.Profile.placeholderImage) 287 | .linked($profileImageURL.flatMapLatest { $0?.getImage(failover: Resource.Profile.placeholderImage) ?? .just(Resource.Profile.placeholderImage) }, keyPath: \.image) 288 | .makeContentMode(.scaleAspectFill) 289 | .frame(width: 48, height: 48) 290 | .cornerRadius(24) 291 | .background(.Palette.gray05) 292 | Text("") 293 | .linked($username, keyPath: \.text) 294 | .textAligned(.center) 295 | .foregroundColor(.darkModeSupporting(.Palette.gray01, .Palette.dkGray01)) 296 | .font(UIFont.systemFont(ofSize: 10, weight: .regular)) 297 | .frame(height: 12) 298 | }.padding(UIEdgeInsets(top: 12, left: 0, bottom: 4, right: 0)) 299 | VStack(alignment: .center) { 300 | Spacer() 301 | Text("LIVE") 302 | .foregroundColor(.Palette.white) 303 | .font(UIFont.systemFont(ofSize: 8, weight: .bold)) 304 | .padding(UIEdgeInsets(top: 2, left: 4, bottom: 2, right: 4)) 305 | .background(.Palette.red) 306 | .cornerRadius(4) 307 | .linked($isLive.not(), keyPath: \.isHidden) 308 | Spacer() 309 | .frame(height: 20) 310 | } 311 | } 312 | ) 313 | } 314 | 315 | required init?(coder: NSCoder) { 316 | fatalError("init(coder:) has not been implemented") 317 | } 318 | } 319 | ``` 320 | 321 | ### Spacer 322 | ```swift 323 | Spacer() 324 | .frame(width: 10) 325 | Spacer() 326 | .frame(height: 20) 327 | ``` 328 | 329 | ### LinearGradient 330 | ``` 331 | ProfileView() 332 | .background( 333 | LinearGradient( 334 | gradient: Gradient( 335 | stops: [ 336 | Stop(color: UIColor.black, location: 1.0), 337 | Stop(color: UIColor.black, location: 0.2), 338 | Stop(color: UIColor.black, location: 0.0) 339 | ]), 340 | startPoint: .top, 341 | endPoint: .bottom 342 | ) 343 | ) 344 | ``` 345 | 346 | ### ViewBuildable - Customize UI, Make reusable component and Design System by confirming ViewBuildable protocol. 347 | 348 | ```swift 349 | struct ProfileView: ViewBuildable { 350 | @Behavior var country: String 351 | @Behavior var name: String 352 | 353 | func build() -> UIView { 354 | VStack { 355 | HStack(alignment: .center, spacing: 12) { 356 | Text("") 357 | .linked($country, keyPath: \.text) 358 | .font(UIFont.systemFont(ofSize: 20, weight: .regular)) 359 | .accessibilityIdentifier("country") 360 | Text("") 361 | .linked($name, keyPath: \.text) 362 | .font(UIFont.systemFont(ofSize: 20, weight: .regular)) 363 | .accessibilityIdentifier("name") 364 | } 365 | } 366 | } 367 | } 368 | ``` 369 | 370 | ## Main Contributors 371 | cruz@hpcnt.com 372 | xeon@hpcnt.com 373 | owen.j@hpcnt.com 374 | dough@hpcnt.com 375 | 376 | ## Dependencies 377 | 378 | - [RxSwift](https://github.com/ReactiveX/RxSwift) 379 | - [RxCocoa](https://github.com/ReactiveX/RxSwift/tree/main/RxCocoa) 380 | - [SnapKit](https://github.com/SnapKit/SnapKit) 381 | -------------------------------------------------------------------------------- /Sources/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperconnect/HypeUI/8e1cd9a00cd615eb5283d29fcf656ebf1022d1ee/Sources/.DS_Store -------------------------------------------------------------------------------- /Sources/HypeUI/Alignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - Alignment 20 | 21 | public enum Alignment { 22 | case topLeading 23 | case top 24 | case topTrailing 25 | case leading 26 | case center 27 | case trailing 28 | case bottomLeading 29 | case bottom 30 | case bottomTrailing 31 | 32 | var isLeading: Bool { 33 | switch self { 34 | case .topLeading, .leading, .bottomLeading: 35 | return true 36 | default: 37 | return false 38 | } 39 | } 40 | 41 | var isTop: Bool { 42 | switch self { 43 | case .topLeading, .top, .topTrailing: 44 | return true 45 | default: 46 | return false 47 | } 48 | } 49 | 50 | var isTrailing: Bool { 51 | switch self { 52 | case .topTrailing, .trailing, .bottomTrailing: 53 | return true 54 | default: 55 | return false 56 | } 57 | } 58 | 59 | var isBottom: Bool { 60 | switch self { 61 | case .bottomLeading, .bottom, .bottomTrailing: 62 | return true 63 | default: 64 | return false 65 | } 66 | } 67 | 68 | var horizontalAlignment: UIStackView.Alignment { 69 | if isTop { 70 | return .top 71 | } else if isBottom { 72 | return .bottom 73 | } else { 74 | return .center 75 | } 76 | } 77 | 78 | var verticalAlignment: UIStackView.Alignment { 79 | if isLeading { 80 | return .leading 81 | } else if isTrailing { 82 | return .trailing 83 | } else { 84 | return .center 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/HypeUI/AlignmentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - AlignmentView 20 | 21 | public struct AlignmentView { 22 | private let alignment: Alignment 23 | private let view: ViewBuildable 24 | 25 | init(alignment: Alignment, view: ViewBuildable) { 26 | self.alignment = alignment 27 | self.view = view 28 | } 29 | } 30 | 31 | // MARK: - AlignmentView (ViewBuildable) 32 | 33 | extension AlignmentView: ViewBuildable { 34 | public func build() -> UIView { 35 | HStack(alignment: alignment.horizontalAlignment) { 36 | Spacer() 37 | .linked(.just(!alignment.isBottom), keyPath: \.isHidden) 38 | VStack(alignment: alignment.verticalAlignment) { 39 | Spacer() 40 | .linked(.just(!alignment.isTrailing), keyPath: \.isHidden) 41 | view 42 | Spacer() 43 | .linked(.just(!alignment.isLeading), keyPath: \.isHidden) 44 | } 45 | Spacer() 46 | .linked(.just(!alignment.isTop), keyPath: \.isHidden) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/HypeUI/Behavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | import RxSwift 19 | import RxCocoa 20 | 21 | // MARK: - Behavior 22 | 23 | @propertyWrapper 24 | public final class Behavior { 25 | 26 | private let internalStore: BehaviorRelay 27 | 28 | public init(wrappedValue initialValue: Value) { 29 | self.internalStore = .init(value: initialValue) 30 | } 31 | 32 | public var projectedValue: Observable { 33 | internalStore.asObservable() 34 | } 35 | 36 | public var wrappedValue: Value { 37 | set { 38 | internalStore.accept(newValue) 39 | } 40 | 41 | get { 42 | internalStore.value 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/HypeUI/Button.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import RxSwift 19 | import RxCocoa 20 | 21 | public typealias Button = UIButton 22 | 23 | // MARK: - Button 24 | 25 | public extension Button { 26 | 27 | /// Button with action closure. 28 | /// - Parameter action: Escaping action closure. 29 | convenience init(action: @escaping () -> Void) { 30 | self.init() 31 | _ = rx.tap.subscribe(onNext: action) 32 | } 33 | 34 | /// Button with action closure. 35 | /// - Parameters: 36 | /// - action: Escaping action closure. 37 | /// - view: Buildable view for button. 38 | convenience init(action: @escaping () -> Void, _ view: () -> ViewBuildable) { 39 | self.init() 40 | let buttonView = view().build() 41 | _ = rx.tap 42 | .map { Void() } 43 | .subscribe(onNext: action) 44 | self.addSubviewWithFit(buttonView.allowsHitTesting(false)) 45 | 46 | _ = Observable 47 | .merge( 48 | rx.controlEvent(.touchDown).map { 0.5 }, 49 | rx.controlEvent(.touchUpInside).map { 1.0 }, 50 | rx.controlEvent(.touchUpOutside).map { 1.0 }, 51 | rx.controlEvent(.touchDragInside).map { 0.5 }, 52 | rx.controlEvent(.touchDragOutside).map { 1.0 } 53 | ) 54 | .bind(to: buttonView.rx.alpha) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/HypeUI/DynamicLinkable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import RxSwift 19 | import RxCocoa 20 | 21 | // MARK: - DynamicLinkable (protocol) 22 | 23 | public protocol DynamicLinkable: NSObject {} 24 | 25 | // MARK: DynamicLinkable 26 | 27 | public extension DynamicLinkable { 28 | /// Bind observable to dynamic member of key path. 29 | /// - Parameters: 30 | /// - stream: Observable for binding. 31 | /// - keyPath: A key path that supports reading from and writing to the resulting value with reference semantics. 32 | /// - Returns: Binded view. 33 | func linked(_ stream: Observable, keyPath: ReferenceWritableKeyPath) -> Self { 34 | let disposeBag = DisposeBag() 35 | 36 | stream.bind(to: self.rx[dynamicMember: keyPath]) 37 | .disposed(by: disposeBag) 38 | 39 | retain(disposeBag) 40 | 41 | return self 42 | } 43 | 44 | /// Bind observable to dynamic member of key path. 45 | /// - Parameters: 46 | /// - stream: Observable for binding. 47 | /// - keyPath: A key path that supports reading from and writing to the resulting value with reference semantics for optional. 48 | /// - Returns: Binded view. 49 | func linked(_ stream: Observable, keyPath: ReferenceWritableKeyPath) -> Self { 50 | let disposeBag = DisposeBag() 51 | 52 | stream.bind(to: self.rx[dynamicMember: keyPath]) 53 | .disposed(by: disposeBag) 54 | retain(disposeBag) 55 | 56 | return self 57 | } 58 | 59 | /// Bind observable to dynamic member of key path. 60 | /// - Parameters: 61 | /// - stream: Observable for binding. 62 | /// - binder: RxCocoa Binder. 63 | /// - Returns: Binded view. 64 | func linked(_ stream: Observable, binder: (Self) -> Binder) -> Self { 65 | let disposeBag = DisposeBag() 66 | 67 | stream.bind(to: binder(self)) 68 | .disposed(by: disposeBag) 69 | 70 | retain(disposeBag) 71 | 72 | return self 73 | } 74 | } 75 | 76 | // MARK: UIView (DynamicLinkable) 77 | 78 | extension UIView: DynamicLinkable {} 79 | 80 | // MARK: CALayer (DynamicLinkable) 81 | 82 | extension CALayer: DynamicLinkable {} 83 | -------------------------------------------------------------------------------- /Sources/HypeUI/Edge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - Edge 20 | 21 | public enum Edge: Int8 { 22 | case top 23 | case leading 24 | case bottom 25 | case trailing 26 | 27 | // MARK: Set 28 | 29 | public struct Set: OptionSet { 30 | public let rawValue: Int8 31 | 32 | public static let top: Set = .init(rawValue: 1 << 0) 33 | 34 | public static let leading: Set = .init(rawValue: 1 << 1) 35 | 36 | public static let bottom: Set = .init(rawValue: 1 << 2) 37 | 38 | public static let trailing: Set = .init(rawValue: 1 << 3) 39 | 40 | public static let all: Set = [.top, .leading, .bottom, .trailing] 41 | 42 | public static let horizontal: Set = [.leading, .trailing] 43 | 44 | public static let vertical: Set = [.top, .bottom] 45 | 46 | public init(rawValue: Int8) { 47 | self.rawValue = rawValue 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/HypeUI/Gradient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - Gradient 20 | 21 | public struct Gradient { 22 | 23 | /// One color stop in the gradient 24 | 25 | public struct Stop { 26 | var color: UIColor 27 | var location: CGFloat 28 | } 29 | 30 | public let stops: [Stop] 31 | 32 | public init(stops: [Stop]) { 33 | self.stops = stops 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/HypeUI/GradientView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - GradientView 20 | 21 | public class GradientView: UIView { 22 | public override class var layerClass: AnyClass { 23 | return CAGradientLayer.self 24 | } 25 | 26 | internal var gradientLayer: CAGradientLayer { 27 | // swiftlint:disable force_cast 28 | return layer as! CAGradientLayer 29 | // swiftlint:enable force_cast 30 | } 31 | 32 | private func update() { 33 | gradientLayer.colors = gradient.stops.map { $0.color.cgColor } 34 | gradientLayer.locations = gradient.stops.map { $0.location as NSNumber } 35 | } 36 | 37 | /// Support dark mode for gradient layer 38 | /// - Parameter previousTraitCollection: A trait collection encapsulates the system traits of an interface's environment. 39 | override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 40 | super.traitCollectionDidChange(previousTraitCollection) 41 | update() 42 | } 43 | 44 | // MARK: Privates 45 | 46 | private let gradient: Gradient 47 | 48 | public init(gradient: Gradient) { 49 | self.gradient = gradient 50 | super.init(frame: .zero) 51 | update() 52 | } 53 | 54 | required init?(coder: NSCoder) { 55 | fatalError("init(coder:) has not been implemented") 56 | } 57 | } 58 | 59 | // MARK: - GradientView (Modifier) 60 | 61 | public extension GradientView { 62 | func startPoint(_ point: UnitPoint) -> Self { 63 | self.gradientLayer.startPoint = point 64 | return self 65 | } 66 | 67 | func endPoint(_ point: UnitPoint) -> Self { 68 | self.gradientLayer.endPoint = point 69 | return self 70 | } 71 | 72 | func type(_ type: CAGradientLayerType) -> Self { 73 | self.gradientLayer.type = type 74 | return self 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/HypeUI/HStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | /// A view that arranges its subviews in a horizontal line. 20 | /// - Parameters: 21 | /// - alignment: The layout of arranged views perpendicular to the stack view’s axis. 22 | /// - spacing: The distance in points between the adjacent edges of the stack view’s arranged views. 23 | /// - content: Build a views from variadic child views 24 | /// - Returns: horizontal stack view 25 | public func HStack(alignment: UIStackView.Alignment = .fill, spacing: CGFloat = 0.0, @ViewArrayBuilder _ content: () -> [UIView]) -> UIStackView { 26 | let stackView = PassthroughStackView(arrangedSubviews: content()) 27 | stackView.axis = .horizontal 28 | stackView.alignment = alignment 29 | stackView.spacing = spacing 30 | return stackView 31 | } 32 | -------------------------------------------------------------------------------- /Sources/HypeUI/Image.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | public typealias Image = UIImageView 20 | 21 | // MARK: - Image 22 | 23 | public extension Image { 24 | 25 | /// A view that displays an image. 26 | /// - Parameter image: (optional) An object that manages image data in your app. 27 | convenience init(_ image: UIImage?) { 28 | self.init() 29 | self.image = image 30 | } 31 | 32 | /// Modify image. 33 | /// - Parameter image: An object that manages image data in your app. 34 | /// - Returns: Modified image. 35 | func imaged(_ image: UIImage) -> Self { 36 | self.image = image 37 | return self 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/HypeUI/LinearGradient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - LinearGradient (ViewBuildable) 20 | 21 | public struct LinearGradient: ViewBuildable { 22 | public let gradient: Gradient 23 | public let startPoint: UnitPoint 24 | public let endPoint: UnitPoint 25 | 26 | /// A linear gradient. 27 | /// - Parameters: 28 | /// - gradient: A color gradient represented as an array of color stops, each having a parametric location value. 29 | /// - startPoint: Start point of gradient. 30 | /// - endPoint: End point of gradient. 31 | public init(gradient: Gradient, startPoint: UnitPoint, endPoint: UnitPoint) { 32 | self.gradient = gradient 33 | self.startPoint = startPoint 34 | self.endPoint = endPoint 35 | } 36 | 37 | public func build() -> UIView { 38 | GradientView(gradient: gradient) 39 | .startPoint(startPoint) 40 | .endPoint(endPoint) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/HypeUI/NSObject+Retain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | private var associatedObjectHandle: UInt8 = 0 20 | 21 | // MARK: - NSObject (retain) 22 | 23 | public extension NSObject { 24 | 25 | /// Increments the receiver’s reference count. 26 | /// - Parameter object: receiver 27 | func retain(_ object: Any) { 28 | let bag: NSMutableArray 29 | if let associated = objc_getAssociatedObject(self, &associatedObjectHandle) as? NSMutableArray { 30 | bag = associated 31 | } else { 32 | bag = NSMutableArray() 33 | objc_setAssociatedObject(self, &associatedObjectHandle, bag, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 34 | } 35 | 36 | bag.add(object) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/HypeUI/PassthroughStackView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - PassthroughStackView 20 | 21 | public final class PassthroughStackView: UIStackView { 22 | public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 23 | return passthrough(point, with: event) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/HypeUI/PassthroughView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - PassthroughView 20 | 21 | public class PassthroughView: UIView { 22 | public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 23 | return passthrough(point, with: event) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/HypeUI/ScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - Axis 20 | 21 | public enum Axis { 22 | case vertical 23 | case horizontal 24 | } 25 | 26 | // MARK: - ScrollView 27 | 28 | // swiftlint:disable identifier_name 29 | 30 | /// A scrollable view. 31 | /// - Parameters: 32 | /// - axis: The scrollable axes of the scroll view. 33 | /// - showsIndicators: A value that indicates whether the scroll view displays the scrollable component of the content offset, in a way that’s suitable for the platform. 34 | /// - content: The buildable content and behavior of the scroll view. 35 | /// - Returns: A scrollable view. 36 | public func ScrollView(_ axis: Axis, showsIndicators: Bool = true, content: () -> ViewBuildable) -> UIScrollView { 37 | let scrollView = UIScrollView() 38 | scrollView.showsVerticalScrollIndicator = showsIndicators 39 | scrollView.showsHorizontalScrollIndicator = showsIndicators 40 | 41 | let contentView = content().build() 42 | scrollView.addSubview(contentView) 43 | contentView.snp.makeConstraints { maker in 44 | maker.directionalEdges.equalToSuperview() 45 | switch axis { 46 | case .horizontal: 47 | maker.height.equalToSuperview() 48 | maker.width.equalToSuperview().priority(.low) 49 | maker.width.greaterThanOrEqualToSuperview() 50 | case .vertical: 51 | maker.width.equalToSuperview() 52 | maker.height.equalToSuperview().priority(.low) 53 | maker.height.greaterThanOrEqualToSuperview() 54 | } 55 | } 56 | return scrollView 57 | } 58 | // swiftlint:enable identifier_name 59 | 60 | // MARK: ScrollView 61 | 62 | public extension UIScrollView { 63 | /// Modify scroll view bounces. 64 | /// - Parameter bounces: A Boolean value that controls whether the scroll view bounces past the edge of content and back again. 65 | /// - Returns: Modified scroll view. 66 | func bounces(_ bounces: Bool) -> Self { 67 | self.bounces = bounces 68 | return self 69 | } 70 | 71 | /// Modify scroll view paging enabled. 72 | /// - Parameter isPagingEnabled: A Boolean value that determines whether paging is enabled for the scroll view. 73 | /// - Returns: Modified scroll view. 74 | func isPagingEnabled(_ isPagingEnabled: Bool) -> Self { 75 | self.isPagingEnabled = isPagingEnabled 76 | return self 77 | } 78 | 79 | /// Modify scroll view enabled. 80 | /// - Parameter isScrollEnabled: A Boolean value that determines whether scrolling is enabled. 81 | /// - Returns: Modified scroll view. 82 | func isScrollEnabled(_ isScrollEnabled: Bool) -> Self { 83 | self.isScrollEnabled = isScrollEnabled 84 | return self 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/HypeUI/Spacer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - Spacer 20 | 21 | public final class Spacer: PassthroughView { 22 | public init() { 23 | super.init(frame: .zero) 24 | 25 | setContentHuggingPriority(.fittingSizeLevel, for: .vertical) 26 | setContentHuggingPriority(.fittingSizeLevel, for: .horizontal) 27 | setContentCompressionResistancePriority(.defaultLow, for: .vertical) 28 | setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 29 | } 30 | 31 | @available(*, deprecated) 32 | public required init?(coder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/HypeUI/Text.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | public typealias Text = UILabel 20 | 21 | // MARK: - Text 22 | 23 | public extension Text { 24 | 25 | /// A view that displays one or more lines of read-only text. 26 | /// - Parameter text: (optional) text. 27 | convenience init(_ text: String?) { 28 | self.init() 29 | self.text = text 30 | } 31 | 32 | /// Modify text font. 33 | /// - Parameter font: An object that provides access to the font’s characteristics. 34 | /// - Returns: Modified text. 35 | func font(_ font: UIFont) -> Text { 36 | self.font = font 37 | return self 38 | } 39 | 40 | /// Modify foreground color. 41 | /// - Parameter color: An object that stores color data and sometimes opacity. 42 | /// - Returns: Modified text. 43 | func foregroundColor(_ color: UIColor) -> Text { 44 | self.textColor = color 45 | return self 46 | } 47 | 48 | /// Modify text alignment 49 | /// - Parameter textAlignment: Constants that specify text alignment. 50 | /// - Returns: Modified text. 51 | func textAligned(_ textAlignment: NSTextAlignment) -> Text { 52 | self.textAlignment = textAlignment 53 | return self 54 | } 55 | 56 | /// The maximum number of lines that text can occupy in a view. 57 | /// - Parameter numberOfLines: Sets the maximum number of lines that text can occupy in this view. 58 | /// - Returns: Modified text. 59 | func lineLimit(_ numberOfLines: Int) -> Text { 60 | self.numberOfLines = numberOfLines 61 | return self 62 | } 63 | 64 | /// Modify line break mode. 65 | /// - Parameter lineBreakMode: The technique for wrapping and truncating the label’s text. 66 | /// - Returns: Modified text. 67 | func lineBreakMode(_ lineBreakMode: NSLineBreakMode) -> Text { 68 | self.lineBreakMode = lineBreakMode 69 | return self 70 | } 71 | 72 | /// Modify adjustsfontsize to fit width. 73 | /// - Parameter adjustsFontSizeToFit: A Boolean value that determines whether the label reduces the text’s font size to fit the title string into the label’s bounding rectangle. 74 | /// - Returns: Modified text. 75 | func adjustedFontSize(_ adjustsFontSizeToFit: Bool) -> Text { 76 | self.adjustsFontSizeToFitWidth = adjustsFontSizeToFit 77 | return self 78 | } 79 | 80 | /// Modify minimum scale factor. 81 | /// - Parameter factor: The minimum scale factor for the label’s text. 82 | /// - Returns: Modified text. 83 | func minimumScaleFactor(_ factor: CGFloat) -> Text { 84 | self.minimumScaleFactor = factor 85 | return self 86 | } 87 | 88 | /// Sets the preferred maximum layout width for the Text object 89 | /// This function allows for chaining by returning the modified Text instance. 90 | /// 91 | /// - Parameter width: The maximum width that the Text object should try to adhere to in its layout. 92 | /// - Returns: The Text instance with the updated preferred maximum layout width, allowing for method chaining. 93 | func preferredMaxLayoutWidth(_ width: CGFloat) -> Text { 94 | preferredMaxLayoutWidth = width 95 | return self 96 | } 97 | 98 | /// Sets the baseline adjustment and returns the Text for method chaining. 99 | /// 100 | /// - Parameter adjustment: The baseline adjustment to apply to the Text. 101 | /// - Returns: The Text with the updated baseline adjustment. 102 | func baselineAdjusted(_ adjustment: UIBaselineAdjustment) -> Text { 103 | baselineAdjustment = adjustment 104 | return self 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/HypeUI/UIView+Passthrough.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - UIView (Passthrought) 20 | 21 | extension UIView { 22 | 23 | /// [Hit-Testing in iOS](http://smnh.me/hit-testing-in-ios/) 24 | func passthrough(_ point: CGPoint, with event: UIEvent?) -> UIView? { 25 | guard isUserInteractionEnabled else { return nil } 26 | guard isHidden == false else { return nil } 27 | guard alpha > 0.01 else { return nil } 28 | guard self.point(inside: point, with: event) else { return nil } 29 | 30 | for subview in subviews.reversed() { 31 | if let view = subview.hitTest(subview.convert(point, from: self), with: event) { 32 | return view 33 | } 34 | } 35 | return nil 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Sources/HypeUI/UIView+Search.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - UIView (Search) 20 | 21 | public extension UIView { 22 | 23 | /// DFS search for finding specific view by identifier. 24 | /// - Parameter identifier: A string that identifies the element. 25 | /// - Returns: Found view. 26 | func findView(identifier: String) -> UIView? { 27 | if accessibilityIdentifier == identifier { 28 | return self 29 | } 30 | for subview in subviews { 31 | if let found = subview.findView(identifier: identifier) { 32 | return found 33 | } 34 | } 35 | return nil 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/HypeUI/UIView+ViewBuildable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - UIView (ViewBuildable) 20 | 21 | extension UIView: ViewBuildable { 22 | public func build() -> UIView { 23 | return self 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/HypeUI/UnitPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | public typealias UnitPoint = CGPoint 20 | 21 | // MARK: - UnitPoint 22 | 23 | public extension UnitPoint { 24 | static let center = UnitPoint(x: 0.5, y: 0.5) 25 | static let left = UnitPoint(x: 0.0, y: 0.5) 26 | static let right = UnitPoint(x: 1.0, y: 0.5) 27 | static let top = UnitPoint(x: 0.5, y: 0.0) 28 | static let bottom = UnitPoint(x: 0.5, y: 1.0) 29 | } 30 | -------------------------------------------------------------------------------- /Sources/HypeUI/VStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | /// A view that arranges its subviews in a vertical line. 20 | /// - Parameters: 21 | /// - alignment: The layout of arranged views perpendicular to the stack view’s axis. 22 | /// - spacing: The distance in points between the adjacent edges of the stack view’s arranged views. 23 | /// - content: Build a views from variadic child views 24 | /// - Returns: vertical stack view 25 | public func VStack(alignment: UIStackView.Alignment = .fill, spacing: CGFloat = 0.0, @ViewArrayBuilder _ content: () -> [UIView]) -> UIStackView { 26 | let stackView = PassthroughStackView(arrangedSubviews: content()) 27 | stackView.axis = .vertical 28 | stackView.alignment = alignment 29 | stackView.spacing = spacing 30 | return stackView 31 | } 32 | -------------------------------------------------------------------------------- /Sources/HypeUI/View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | import SnapKit 19 | 20 | // MARK: - UIView 21 | 22 | public extension UIView { 23 | 24 | /// Sets the priority with which a view resists being made larger than its intrinsic size. 25 | /// - Parameter priority: The new priority. 26 | /// - Returns: Modified view. 27 | func setHContentHugging(priority: UILayoutPriority) -> Self { 28 | self.setContentHuggingPriority(priority, for: .horizontal) 29 | return self 30 | } 31 | 32 | /// Sets the priority with which a view resists being made larger than its intrinsic size. 33 | /// - Parameter priority: The new priority. 34 | /// - Returns: Modified view. 35 | func setVContentHugging(priority: UILayoutPriority) -> Self { 36 | self.setContentHuggingPriority(priority, for: .vertical) 37 | return self 38 | } 39 | 40 | /// Sets the priority with which a view resists being made smaller than its intrinsic size. 41 | /// - Parameter priority: The new priority. 42 | /// - Returns: Modified view. 43 | func setHContentCompressionResistance(priority: UILayoutPriority) -> Self { 44 | self.setContentCompressionResistancePriority(priority, for: .horizontal) 45 | return self 46 | } 47 | 48 | /// Sets the priority with which a view resists being made smaller than its intrinsic size. 49 | /// - Parameter priority: The new priority. 50 | /// - Returns: modified view 51 | func setVContentCompressionResistance(priority: UILayoutPriority) -> Self { 52 | self.setContentCompressionResistancePriority(priority, for: .vertical) 53 | return self 54 | } 55 | 56 | /// Modify the ratio of the size of the view. 57 | /// - Parameter ratio: Size ratio. 58 | /// - Returns: Modified view. 59 | func makeRatio(_ ratio: CGFloat) -> Self { 60 | self.snp.makeConstraints { maker in 61 | maker.height.equalTo(self.snp.width).multipliedBy(ratio) 62 | } 63 | return self 64 | } 65 | 66 | /// Modify corner radius. 67 | /// - Parameter radius: The radius to use when drawing rounded corners for the layer’s background. 68 | /// - Returns: Modified view. 69 | func cornerRadius(_ radius: CGFloat) -> Self { 70 | self.applyRound(radius) 71 | return self 72 | } 73 | 74 | /// Modify corner radius. 75 | /// - Parameter radius: The radius to use when drawing rounded corners for the layer’s background. 76 | /// - Parameter corners: A mask used to apply a radius to a specific corner. 77 | /// - Returns: Modified view. 78 | func cornerRadius(_ radius: CGFloat, corners: CACornerMask? = nil) -> Self { 79 | self.applyRound(radius, corners: corners) 80 | return self 81 | } 82 | 83 | /// Modify corner radius. 84 | /// - Parameter radius: The radius to use when drawing rounded corners for the layer’s background. 85 | /// - Parameter corners: A mask used to apply a radius to a specific corner. 86 | private func applyRound(_ radius: CGFloat, corners: CACornerMask? = nil) { 87 | if let corners = corners { 88 | self.layer.maskedCorners = corners 89 | } 90 | self.layer.cornerRadius = radius 91 | self.layer.masksToBounds = true 92 | } 93 | 94 | /// Modify border style. 95 | /// - Parameters: 96 | /// - color: The color of the layer’s border. 97 | /// - width: The width of the layer’s border. 98 | /// - Returns: Modified view 99 | func border(_ color: UIColor, width: CGFloat) -> Self { 100 | self.layer.borderColor = color.cgColor 101 | self.layer.borderWidth = width 102 | return self 103 | } 104 | 105 | /// Modifiy background color. 106 | /// - Parameter color: An object that stores color data and sometimes opacity. 107 | /// - Returns: Modified view 108 | func background(_ color: UIColor) -> Self { 109 | self.backgroundColor = color 110 | return self 111 | } 112 | 113 | /// Modify content mode. 114 | /// - Parameter contentMode: Options to specify how a view adjusts its content when its size changes. 115 | /// - Returns: Modified view. 116 | func makeContentMode(_ contentMode: ContentMode) -> Self { 117 | self.contentMode = contentMode 118 | return self 119 | } 120 | 121 | /// Positions this view within an invisible frame with the specified size. 122 | /// - Parameters: 123 | /// - width: A fixed width for the resulting view. If width is nil, the resulting view assumes this view’s sizing behavior. 124 | /// - height: A fixed height for the resulting view. If height is nil, the resulting view assumes this view’s sizing behavior. 125 | /// - Returns: Modified view. 126 | func frame(width: CGFloat? = nil, height: CGFloat? = nil) -> Self { 127 | if let width = width { 128 | widthAnchor.constraint(equalToConstant: width).isActive = true 129 | } 130 | 131 | if let height = height { 132 | heightAnchor.constraint(equalToConstant: height).isActive = true 133 | } 134 | 135 | return self 136 | } 137 | 138 | /// Adds an equal padding amount to specific edges of this view. 139 | /// - Parameter inset: The inset distances for views. 140 | /// - Returns: Modified view. 141 | func padding(_ inset: UIEdgeInsets) -> UIView { 142 | let view = UIView() 143 | view.addSubview(self) 144 | self.snp.makeConstraints { maker in 145 | maker.directionalEdges.equalTo(inset) 146 | } 147 | return view 148 | } 149 | 150 | /// Adds an equal padding amount to specific edges of this view. 151 | /// - Parameters: 152 | /// - edges: The set of edges to pad for this view. 153 | /// - length: An amount, given in points, to pad this view on the specified edges. 154 | /// - Returns: Modified view. 155 | func padding(_ edges: Edge.Set, _ length: CGFloat) -> UIView { 156 | let paddingView = UIView() 157 | self.translatesAutoresizingMaskIntoConstraints = false 158 | paddingView.addSubview(self) 159 | 160 | NSLayoutConstraint.activate([ 161 | topAnchor.constraint(equalTo: paddingView.topAnchor, constant: edges.contains(.top) ? length : 0), 162 | leadingAnchor.constraint(equalTo: paddingView.leadingAnchor, constant: edges.contains(.leading) ? length : 0), 163 | bottomAnchor.constraint(equalTo: paddingView.bottomAnchor, constant: edges.contains(.bottom) ? -length : 0), 164 | trailingAnchor.constraint(equalTo: paddingView.trailingAnchor, constant: edges.contains(.trailing) ? -length : 0), 165 | ]) 166 | 167 | return paddingView 168 | } 169 | 170 | /// Configures whether this view participates in hit test operations. 171 | /// - Parameter allows: A value type whose instances are either true or false. 172 | /// - Returns: Modified view. 173 | func allowsHitTesting(_ allows: Bool) -> Self { 174 | self.isUserInteractionEnabled = allows 175 | return self 176 | } 177 | 178 | /// Adds a view to the end of the receiver’s list of subviews with fit 179 | /// - Parameter view: Modified view. 180 | func addSubviewWithFit(_ view: UIView) { 181 | addSubview(view) 182 | view.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } 183 | } 184 | 185 | /// Setting sublayers are clipped to the layer’s bounds. 186 | /// - Parameter masksToBounds: A Boolean indicating whether sublayers are clipped to the layer’s bounds. 187 | /// - Returns: Modified view. 188 | func masksToBounds(_ masksToBounds: Bool) -> Self { 189 | layer.masksToBounds = masksToBounds 190 | return self 191 | } 192 | 193 | /// Uses the string you specify to identify the view. 194 | /// - Parameter identifier: A string that identifies the element. 195 | /// - Returns: Modified view. 196 | func accessibilityIdentifier(_ identifier: String) -> Self { 197 | self.accessibilityIdentifier = identifier 198 | return self 199 | } 200 | 201 | /// Layers the views that you specify in front of this view. 202 | /// - Parameters: 203 | /// - alignment: An alignment in both axes. 204 | /// - view: Buildable view content. 205 | /// - Returns: Modified view. 206 | func overlay(alignment: Alignment = .center, view: ViewBuildable) -> UIView { 207 | ZStack { 208 | self 209 | AlignmentView(alignment: alignment, view: view) 210 | } 211 | } 212 | 213 | /// Layers the views that you specify behind this view. 214 | /// - Parameters: 215 | /// - alignment: An alignment in both axes. 216 | /// - view: Buildable view content. 217 | /// - Returns: Modified view. 218 | func background(alignment: Alignment = .center, view: ViewBuildable) -> UIView { 219 | ZStack { 220 | AlignmentView(alignment: alignment, view: view) 221 | self 222 | } 223 | } 224 | 225 | /// Wraps and centers the current view in a new parent View. 226 | /// 227 | /// - Returns: The new parent View with this view centered inside. 228 | func center() -> UIView { 229 | let view = UIView() 230 | view.addSubview(self) 231 | snp.makeConstraints { maker in 232 | maker.center.equalToSuperview() 233 | } 234 | return view 235 | } 236 | 237 | /// Applies a tint color to the view and returns the modified view for chaining. 238 | /// 239 | /// This extension function sets the `tintColor` property of the view to a specified color, allowing for easy modification of the view's appearance in a method chaining style. 240 | /// 241 | /// - Parameter tintColor: The color to set as the tint color of the view. 242 | /// - Returns: The view instance with the updated tint color. 243 | func tint(_ tintColor: UIColor) -> Self { 244 | self.tintColor = tintColor 245 | return self 246 | } 247 | 248 | /// Sets the opacity of the view and returns the modified view for chaining. 249 | /// 250 | /// This function adjusts the `alpha` property of the view to control its opacity, allowing for fluent style coding. 251 | /// The return type of 'Self' ensures that the function can be used with any subclass of UIView that inherits this method. 252 | /// 253 | /// - Parameter opacity: The opacity level to set, where 1 is fully opaque and 0 is completely transparent. 254 | /// - Returns: The view instance with the updated opacity. 255 | func opacity(_ opacity: CGFloat) -> Self { 256 | alpha = opacity 257 | return self 258 | } 259 | 260 | /// Applies a scale transformation to the view. 261 | /// 262 | /// - Parameters: 263 | /// - scaleX: The scale factor in the x-axis. 264 | /// - scaleY: The scale factor in the y-axis. 265 | /// - Returns: The view with the scaling applied, allowing for chaining. 266 | func scaleEffect(scaleX: CGFloat, scaleY: CGFloat) -> Self { 267 | self.transform = CGAffineTransform(scaleX: scaleX, y: scaleY) 268 | return self 269 | } 270 | 271 | /// Applies a rotation transformation to the view. 272 | /// 273 | /// - Parameters: 274 | /// - angle: The rotation angle in degrees. 275 | /// - anchor: The anchor point of the rotation, default is center. 276 | /// - Returns: The view with the rotation applied, allowing for chaining. 277 | func rotationEffect(_ angle: CGFloat, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5)) -> Self { 278 | var transform = CGAffineTransform.identity 279 | transform = transform.translatedBy(x: anchor.x, y: anchor.y) 280 | transform = transform.rotated(by: angle * CGFloat.pi / 180) 281 | transform = transform.translatedBy(x: -anchor.x, y: -anchor.y) 282 | self.transform = transform 283 | return self 284 | } 285 | } 286 | 287 | // MARK: - UIStackView 288 | 289 | public extension UIStackView { 290 | 291 | /// Modify stack's distribution layout. 292 | /// - Parameter distribution: The layout that defines the size and position of the arranged views along the stack view’s axis. 293 | /// - Returns: Modified stack view. 294 | func distributed(_ distribution: UIStackView.Distribution) -> Self { 295 | self.distribution = distribution 296 | return self 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /Sources/HypeUI/ViewArrayBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - ViewArrayBuilder 20 | 21 | @resultBuilder 22 | public struct ViewArrayBuilder { 23 | 24 | /// Build a views from variadic child views 25 | /// - Parameter views: An buildable variadic child views 26 | /// - Returns: An array of views that are composed of variadic child views 27 | public static func buildBlock(_ views: ViewBuildable...) -> [UIView] { 28 | views.map { $0.build() } 29 | } 30 | 31 | 32 | /// Build a views from array of views 33 | /// - Parameter views: An array of buildable views 34 | /// - Returns: An array of views 35 | public static func buildBlock(_ views: [ViewBuildable]) -> [UIView] { 36 | views.map { $0.build() } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/HypeUI/ViewBuildable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | // MARK: - ViewBuildable (protocol) 20 | 21 | public protocol ViewBuildable { 22 | func build() -> UIView 23 | } 24 | -------------------------------------------------------------------------------- /Sources/HypeUI/ZStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | /// A view that overlays its subviews, aligning them in both axes. 20 | /// - Parameter content: Build a views from variadic child views. 21 | /// - Returns: A view that overlays content views. 22 | public func ZStack(@ViewArrayBuilder _ content: () -> [UIView]) -> UIView { 23 | let view = UIView() 24 | content().forEach(view.addSubviewWithFit) 25 | return view 26 | } 27 | -------------------------------------------------------------------------------- /Tests/HypeUITests/AlignmentTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | // MARK: - AlignmentTests 20 | 21 | @testable import HypeUI 22 | final class AlignmentTests: XCTestCase { 23 | 24 | func testTopLeadingAlignment() { 25 | let sut = Alignment.topLeading 26 | 27 | XCTAssertTrue(sut.isTop) 28 | XCTAssertTrue(sut.isLeading) 29 | 30 | XCTAssertFalse(sut.isBottom) 31 | XCTAssertFalse(sut.isTrailing) 32 | 33 | XCTAssertEqual(sut.horizontalAlignment, .top) 34 | XCTAssertEqual(sut.verticalAlignment, .leading) 35 | } 36 | 37 | func testTopAlignment() { 38 | let sut = Alignment.top 39 | 40 | XCTAssertTrue(sut.isTop) 41 | XCTAssertFalse(sut.isLeading) 42 | 43 | XCTAssertFalse(sut.isBottom) 44 | XCTAssertFalse(sut.isTrailing) 45 | 46 | XCTAssertEqual(sut.horizontalAlignment, .top) 47 | XCTAssertEqual(sut.verticalAlignment, .center) 48 | } 49 | 50 | func testTopTrailingAlignment() { 51 | let sut = Alignment.topTrailing 52 | 53 | XCTAssertTrue(sut.isTop) 54 | XCTAssertFalse(sut.isLeading) 55 | 56 | XCTAssertFalse(sut.isBottom) 57 | XCTAssertTrue(sut.isTrailing) 58 | 59 | XCTAssertEqual(sut.horizontalAlignment, .top) 60 | XCTAssertEqual(sut.verticalAlignment, .trailing) 61 | } 62 | 63 | func testLeadingAlignment() { 64 | let sut = Alignment.leading 65 | 66 | XCTAssertFalse(sut.isTop) 67 | XCTAssertTrue(sut.isLeading) 68 | 69 | XCTAssertFalse(sut.isBottom) 70 | XCTAssertFalse(sut.isTrailing) 71 | 72 | XCTAssertEqual(sut.horizontalAlignment, .center) 73 | XCTAssertEqual(sut.verticalAlignment, .leading) 74 | } 75 | 76 | func testCenterAlignment() { 77 | let sut = Alignment.center 78 | 79 | XCTAssertFalse(sut.isTop) 80 | XCTAssertFalse(sut.isLeading) 81 | 82 | XCTAssertFalse(sut.isBottom) 83 | XCTAssertFalse(sut.isTrailing) 84 | 85 | XCTAssertEqual(sut.horizontalAlignment, .center) 86 | XCTAssertEqual(sut.verticalAlignment, .center) 87 | } 88 | 89 | func testTrailingAlignment() { 90 | let sut = Alignment.trailing 91 | 92 | XCTAssertFalse(sut.isTop) 93 | XCTAssertFalse(sut.isLeading) 94 | 95 | XCTAssertFalse(sut.isBottom) 96 | XCTAssertTrue(sut.isTrailing) 97 | 98 | XCTAssertEqual(sut.horizontalAlignment, .center) 99 | XCTAssertEqual(sut.verticalAlignment, .trailing) 100 | } 101 | 102 | func testBottomLeadingAlignment() { 103 | let sut = Alignment.bottomLeading 104 | 105 | XCTAssertFalse(sut.isTop) 106 | XCTAssertTrue(sut.isLeading) 107 | 108 | XCTAssertTrue(sut.isBottom) 109 | XCTAssertFalse(sut.isTrailing) 110 | 111 | XCTAssertEqual(sut.horizontalAlignment, .bottom) 112 | XCTAssertEqual(sut.verticalAlignment, .leading) 113 | } 114 | 115 | func testBottomAlignment() { 116 | let sut = Alignment.bottom 117 | 118 | XCTAssertFalse(sut.isTop) 119 | XCTAssertFalse(sut.isLeading) 120 | 121 | XCTAssertTrue(sut.isBottom) 122 | XCTAssertFalse(sut.isTrailing) 123 | 124 | XCTAssertEqual(sut.horizontalAlignment, .bottom) 125 | XCTAssertEqual(sut.verticalAlignment, .center) 126 | } 127 | 128 | func testBottomTrailingAlignment() { 129 | let sut = Alignment.bottomTrailing 130 | 131 | XCTAssertFalse(sut.isTop) 132 | XCTAssertFalse(sut.isLeading) 133 | 134 | XCTAssertTrue(sut.isBottom) 135 | XCTAssertTrue(sut.isTrailing) 136 | 137 | XCTAssertEqual(sut.horizontalAlignment, .bottom) 138 | XCTAssertEqual(sut.verticalAlignment, .trailing) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Tests/HypeUITests/HStackTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | // MARK: - HStackTests 20 | 21 | @testable import HypeUI 22 | final class HStackTests: XCLayoutTestCase { 23 | 24 | func testHStackImageWithText() { 25 | // given 26 | let spacing: CGFloat = 5 27 | let image = Image() 28 | let text = Text("*****") 29 | let spacer = Spacer() 30 | 31 | // when 32 | let sut = HStack(alignment: .center, spacing: spacing) { 33 | image 34 | .frame(width: 24, height: 24) 35 | text 36 | .font(UIFont.systemFont(ofSize: 20, weight: .bold)) 37 | spacer 38 | } 39 | contentView.addSubviewWithFit(sut) 40 | contentView.layoutIfNeeded() 41 | 42 | // then 43 | XCTAssertEqual(image.frame.width, 24) 44 | XCTAssertEqual(image.frame.height, 24) 45 | XCTAssertLessThan(image.frame.width, spacer.frame.width) 46 | XCTAssertLessThan(text.frame.width, spacer.frame.width) 47 | } 48 | 49 | func testHStackCenterAlignmentWithSpacing() { 50 | // given 51 | let spacing: CGFloat = 10 52 | let title = Text("Title") 53 | .font(UIFont.systemFont(ofSize: 10, weight: .bold)) 54 | let spacer = Spacer() 55 | 56 | // when 57 | let sut = HStack(alignment: .center, spacing: spacing) { 58 | title 59 | spacer 60 | } 61 | contentView.addSubviewWithFit(sut) 62 | contentView.layoutIfNeeded() 63 | 64 | // then 65 | XCTAssertEqual(title.frame.maxX.rounded() + spacing, spacer.frame.minX.rounded()) 66 | XCTAssertEqual(title.frame.midY, spacer.frame.midY) 67 | XCTAssertLessThan(title.frame.width, spacer.frame.width) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/HypeUITests/ScrollViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | // MARK: - ScrollViewTests 20 | 21 | @testable import HypeUI 22 | final class ScrollViewTests: XCLayoutTestCase { 23 | func testHorizontalScrollViewSmallContentSize() { 24 | // given 25 | let scrollView = ScrollView(.horizontal, showsIndicators: false) { 26 | HStack(alignment: .center, spacing: 20) { 27 | Text() 28 | .frame(width: 100) 29 | Image() 30 | .frame(width: 150) 31 | } 32 | } 33 | 34 | // when 35 | contentView.addSubviewWithFit( 36 | ZStack { 37 | scrollView 38 | } 39 | ) 40 | contentView.layoutIfNeeded() 41 | 42 | // then 43 | XCTAssertEqual(scrollView.frame, contentView.frame) 44 | XCTAssertEqual(scrollView.contentSize, contentView.bounds.size) 45 | } 46 | 47 | func testHorizontalScrollViewLargeContentSize() { 48 | // given 49 | let scrollView = ScrollView(.horizontal, showsIndicators: false) { 50 | HStack(alignment: .center, spacing: 20) { 51 | Text() 52 | .frame(width: 400) 53 | Image() 54 | .frame(width: 650) 55 | } 56 | } 57 | 58 | // when 59 | contentView.addSubviewWithFit( 60 | ZStack { 61 | scrollView 62 | } 63 | ) 64 | contentView.layoutIfNeeded() 65 | 66 | // then 67 | XCTAssertEqual(scrollView.contentSize.height, contentView.bounds.size.height) 68 | XCTAssertEqual(scrollView.frame, contentView.frame) 69 | XCTAssertGreaterThan(scrollView.contentSize.width, contentView.bounds.size.width) 70 | } 71 | 72 | 73 | func testVerticalScrollViewSmallContentSize() { 74 | // given 75 | let scrollView = ScrollView(.vertical, showsIndicators: false) { 76 | VStack(alignment: .center, spacing: 20) { 77 | Text() 78 | .frame(height: 100) 79 | Image() 80 | .frame(height: 150) 81 | } 82 | } 83 | 84 | // when 85 | contentView.addSubviewWithFit( 86 | ZStack { 87 | scrollView 88 | } 89 | ) 90 | contentView.layoutIfNeeded() 91 | 92 | // then 93 | XCTAssertEqual(scrollView.frame, contentView.frame) 94 | XCTAssertEqual(scrollView.contentSize, contentView.bounds.size) 95 | } 96 | 97 | func testVerticalScrollViewLargeContentSize() { 98 | // given 99 | let scrollView = ScrollView(.vertical, showsIndicators: false) { 100 | VStack(alignment: .center, spacing: 20) { 101 | (1...5).map { _ in 102 | HStack(alignment: .center) { 103 | Text() 104 | .frame(height: 100) 105 | Image() 106 | .frame(height: 200) 107 | } 108 | } 109 | } 110 | } 111 | 112 | // when 113 | contentView.addSubviewWithFit( 114 | ZStack { 115 | scrollView 116 | } 117 | ) 118 | contentView.layoutIfNeeded() 119 | 120 | // then 121 | XCTAssertEqual(scrollView.contentSize.width, contentView.bounds.size.width) 122 | XCTAssertEqual(scrollView.frame, contentView.frame) 123 | XCTAssertEqual(scrollView.contentSize.height, 200 * 5 + 20 * 4) 124 | XCTAssertGreaterThan(scrollView.contentSize.height, contentView.bounds.size.height) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Tests/HypeUITests/SpacerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | // MARK: - SpacerTests 20 | 21 | @testable import HypeUI 22 | final class SpacerTests: XCTestCase { 23 | 24 | func testSpacerUILayoutPriority() { 25 | let sut = Spacer() 26 | 27 | testPriority(sut: sut, target: UIView()) 28 | testPriority(sut: sut, target: UILabel()) 29 | testPriority(sut: sut, target: UIButton()) 30 | testPriority(sut: sut, target: UITextView()) 31 | testPriority(sut: sut, target: UITextField()) 32 | testPriority(sut: sut, target: UIStackView()) 33 | testPriority(sut: sut, target: PassthroughView()) 34 | testPriority(sut: sut, target: PassthroughStackView()) 35 | } 36 | 37 | private func testPriority(sut: UIView, target: UIView) { 38 | XCTAssertLessThan(sut.contentHuggingPriority(for: .vertical), target.contentHuggingPriority(for: .vertical)) 39 | XCTAssertLessThan(sut.contentHuggingPriority(for: .horizontal), target.contentHuggingPriority(for: .horizontal)) 40 | XCTAssertLessThan(sut.contentCompressionResistancePriority(for: .vertical), target.contentCompressionResistancePriority(for: .vertical)) 41 | XCTAssertLessThan(sut.contentCompressionResistancePriority(for: .horizontal), target.contentCompressionResistancePriority(for: .horizontal)) 42 | } 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /Tests/HypeUITests/TextTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | // MARK: - TextTests 20 | 21 | @testable import HypeUI 22 | final class TextTests: XCLayoutTestCase { 23 | 24 | func testOptionalNilTextModifyFont() { 25 | // Given 26 | let sut = Text(nil) 27 | 28 | // When 29 | let output = sut.font(UIFont.systemFont(ofSize: 12, weight: .regular)) 30 | 31 | // Then 32 | XCTAssertNil(sut.text) 33 | XCTAssertEqual(sut, output) 34 | } 35 | 36 | func testNormalTextModifyFont() { 37 | // Given 38 | let title = "🔥 Hello HypeUI" 39 | let sut = Text(title) 40 | 41 | // When 42 | let output = sut.font(UIFont.systemFont(ofSize: 12, weight: .regular)) 43 | 44 | // Then 45 | XCTAssertEqual(sut.text, title) 46 | XCTAssertEqual(sut, output) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/HypeUITests/UIView+PassthroughTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | // MARK: - UIView_PassthroughTests 20 | 21 | @testable import HypeUI 22 | final class UIView_PassthroughTests: XCTestCase { 23 | enum Constant { 24 | static let defaultFrame = CGRect(origin: .zero, size: CGSize(width: 100, height: 100)) 25 | } 26 | 27 | func testPassThroughView() { 28 | // given 29 | let sut = UIView(frame: Constant.defaultFrame) 30 | sut.isHidden = false 31 | 32 | // when 33 | let output = sut.passthrough(CGPoint(x: 10, y: 10), with: nil) 34 | 35 | // then 36 | XCTAssertNil(output) 37 | } 38 | 39 | func testPassThroughHiddenView() { 40 | // given 41 | let sut = UIView(frame: Constant.defaultFrame) 42 | sut.isHidden = true 43 | 44 | // when 45 | let output = sut.passthrough(CGPoint(x: 10, y: 10), with: nil) 46 | 47 | // then 48 | XCTAssertNil(output) 49 | } 50 | 51 | func testPassThroughViewWithSubView() { 52 | // given 53 | let sut = UIView(frame: Constant.defaultFrame) 54 | let target = UIView(frame: Constant.defaultFrame) 55 | sut.isHidden = false 56 | sut.addSubview(target) 57 | 58 | // when 59 | let output = sut.passthrough(CGPoint(x: 10, y: 10), with: nil) 60 | 61 | // then 62 | XCTAssertNotNil(output) 63 | XCTAssertEqual(output, target) 64 | } 65 | 66 | func testPassThroughHiddenViewWithSubview() { 67 | // given 68 | let sut = UIView(frame: Constant.defaultFrame) 69 | let target = UIView(frame: Constant.defaultFrame) 70 | sut.isHidden = true 71 | sut.addSubview(target) 72 | 73 | // when 74 | let output = sut.passthrough(CGPoint(x: 10, y: 10), with: nil) 75 | 76 | // then 77 | XCTAssertNil(output) 78 | } 79 | 80 | func testPassThroughUserInteractionEnabledViewWithSubview() { 81 | // given 82 | let sut = UIView(frame: Constant.defaultFrame) 83 | let target = UIView(frame: Constant.defaultFrame) 84 | sut.isHidden = false 85 | sut.isUserInteractionEnabled = true 86 | sut.addSubview(target) 87 | 88 | // when 89 | let output = sut.passthrough(CGPoint(x: 10, y: 10), with: nil) 90 | 91 | // then 92 | XCTAssertNotNil(output) 93 | XCTAssertEqual(output, target) 94 | } 95 | 96 | func testPassThroughUserInteractionDisabledViewWithSubview() { 97 | // given 98 | let sut = UIView(frame: Constant.defaultFrame) 99 | let target = UIView(frame: Constant.defaultFrame) 100 | sut.isHidden = false 101 | sut.isUserInteractionEnabled = false 102 | sut.addSubview(target) 103 | 104 | // when 105 | let output = sut.passthrough(CGPoint(x: 10, y: 10), with: nil) 106 | 107 | // then 108 | XCTAssertNil(output) 109 | } 110 | 111 | func testPassThroughAlphaShownViewWithSubview() { 112 | // given 113 | let sut = UIView(frame: Constant.defaultFrame) 114 | let target = UIView(frame: Constant.defaultFrame) 115 | sut.isHidden = false 116 | sut.addSubview(target) 117 | 118 | do { 119 | // when 120 | sut.alpha = 0.1 121 | let output = sut.passthrough(CGPoint(x: 10, y: 10), with: nil) 122 | 123 | // then 124 | XCTAssertNotNil(output) 125 | XCTAssertEqual(output, target) 126 | } 127 | 128 | do { 129 | // when 130 | sut.alpha = 1.0 131 | let output = sut.passthrough(CGPoint(x: 10, y: 10), with: nil) 132 | 133 | // then 134 | XCTAssertNotNil(output) 135 | XCTAssertEqual(output, target) 136 | } 137 | } 138 | 139 | func testPassThroughAlphaHiddenViewWithSubview() { 140 | // given 141 | let sut = UIView(frame: Constant.defaultFrame) 142 | let target = UIView(frame: Constant.defaultFrame) 143 | sut.isHidden = false 144 | sut.addSubview(target) 145 | 146 | do { 147 | // when 148 | sut.alpha = 0.0 149 | let output = sut.passthrough(CGPoint(x: 10, y: 10), with: nil) 150 | 151 | // then 152 | XCTAssertNil(output) 153 | } 154 | 155 | do { 156 | // when 157 | sut.alpha = 0.01 158 | let output = sut.passthrough(CGPoint(x: 10, y: 10), with: nil) 159 | 160 | // then 161 | XCTAssertNil(output) 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Tests/HypeUITests/ViewArrayBuilderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | // MARK: - ViewArrayBuilderTests 20 | 21 | @testable import HypeUI 22 | final class ViewArrayBuilderTests: XCTestCase { 23 | func testViewArrayBuilder() { 24 | // given 25 | let label = UILabel() 26 | let button = UIButton() 27 | let image = UIImageView() 28 | let expect = [label, button, image] 29 | 30 | // when 31 | let output = ViewArrayBuilder.buildBlock(expect) 32 | 33 | // then 34 | XCTAssertEqual(output, expect) 35 | } 36 | 37 | func testViewArrayResultBuilder() { 38 | // given 39 | let label = UILabel() 40 | let button = UIButton() 41 | let image = UIImageView() 42 | let expect = [label, button, image] 43 | 44 | // when 45 | let output = ViewArrayBuilder.buildBlock(label, button, image) 46 | 47 | // then 48 | XCTAssertEqual(output, expect) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/HypeUITests/ViewBuildableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | // MARK: - ViewBuildableTests 20 | 21 | @testable import HypeUI 22 | final class ViewBuildableTests: XCLayoutTestCase { 23 | 24 | func testHStackWithViewBuildable() { 25 | struct ProfileView: ViewBuildable { 26 | @Behavior var country: String 27 | @Behavior var name: String 28 | 29 | func build() -> UIView { 30 | VStack { 31 | HStack(alignment: .center, spacing: 12) { 32 | Text("") 33 | .linked($country, keyPath: \.text) 34 | .font(UIFont.systemFont(ofSize: 20, weight: .regular)) 35 | .accessibilityIdentifier("country") 36 | Text("") 37 | .linked($name, keyPath: \.text) 38 | .font(UIFont.systemFont(ofSize: 20, weight: .regular)) 39 | .accessibilityIdentifier("name") 40 | } 41 | } 42 | } 43 | } 44 | 45 | // Given 46 | 47 | let sut = ProfileView(country: "🇪🇸", name: "Español") 48 | 49 | // When 50 | 51 | contentView.addSubviewWithFit( 52 | VStack { 53 | HStack { 54 | sut 55 | Spacer() 56 | }.build().build().build() 57 | } 58 | ) 59 | 60 | // Then 61 | 62 | let countryText = contentView.findView(identifier: "country") as? Text 63 | let nameText = contentView.findView(identifier: "name") as? Text 64 | XCTAssertEqual(countryText?.text, "🇪🇸") 65 | XCTAssertEqual(nameText?.text, "Español") 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/HypeUITests/ViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | import UIKit 19 | 20 | // MARK: - ViewTests 21 | 22 | @testable import HypeUI 23 | final class ViewTests: XCLayoutTestCase { 24 | 25 | func testPaddingWithUIEdgeInsets() { 26 | // given 27 | let sut = HStack(spacing: 5) { 28 | Image() 29 | .frame(width: 20, height: 20) 30 | Text() 31 | .frame(width: 100, height: 20) 32 | }.padding(UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)) 33 | 34 | contentView.addSubviewWithFit( 35 | VStack(alignment: .center) { 36 | sut 37 | Spacer() 38 | } 39 | ) 40 | contentView.layoutIfNeeded() 41 | 42 | // then 43 | XCTAssertEqual(sut.frame.size, CGSize(width: 20 + 125 + 20, height: 10 + 20 + 10)) 44 | XCTAssertEqual(sut.frame.midX, sut.frame.midX) 45 | 46 | // https://github.com/SnapKit/SnapKit/blob/develop/Tests/SnapKitTests/Tests.swift#L95 47 | XCTAssertNotNil(sut.constraints.first { $0.firstAttribute == .leading && $0.secondAttribute == .leading }) 48 | XCTAssertNotNil(sut.constraints.first { $0.firstAttribute == .trailing && $0.secondAttribute == .trailing }) 49 | XCTAssertNotNil(sut.constraints.first { $0.firstAttribute == .top && $0.secondAttribute == .top }) 50 | XCTAssertNotNil(sut.constraints.first { $0.firstAttribute == .bottom && $0.secondAttribute == .bottom }) 51 | } 52 | 53 | func testPaddingWithEdges() { 54 | // given 55 | let sut = HStack(spacing: 5) { 56 | Image() 57 | .frame(width: 20, height: 20) 58 | Text() 59 | .frame(width: 100, height: 20) 60 | }.padding(.all, 20) 61 | 62 | contentView.addSubviewWithFit( 63 | VStack(alignment: .center) { 64 | sut 65 | Spacer() 66 | } 67 | ) 68 | contentView.layoutIfNeeded() 69 | 70 | // then 71 | XCTAssertEqual(sut.frame.size, CGSize(width: 20 + 125 + 20, height: 20 + 20 + 20)) 72 | XCTAssertEqual(sut.frame.midX, sut.frame.midX) 73 | 74 | // https://github.com/SnapKit/SnapKit/blob/develop/Tests/SnapKitTests/Tests.swift#L95 75 | XCTAssertNotNil(sut.constraints.first { $0.firstAttribute == .leading && $0.secondAttribute == .leading }) 76 | XCTAssertNotNil(sut.constraints.first { $0.firstAttribute == .trailing && $0.secondAttribute == .trailing }) 77 | XCTAssertNotNil(sut.constraints.first { $0.firstAttribute == .top && $0.secondAttribute == .top }) 78 | XCTAssertNotNil(sut.constraints.first { $0.firstAttribute == .bottom && $0.secondAttribute == .bottom }) 79 | } 80 | 81 | func testFrameWidth() { 82 | // given 83 | var sut = Spacer() 84 | 85 | // when 86 | sut = sut.frame(width: 10) 87 | 88 | // then 89 | XCTAssertNotNil(sut.constraints.first { $0.firstAttribute == .width && $0.constant == 10 }) 90 | XCTAssertNil(sut.constraints.first { $0.firstAttribute == .height }) 91 | } 92 | 93 | func testFrameHeight() { 94 | // given 95 | var sut = Spacer() 96 | 97 | // when 98 | sut = sut.frame(height: 10) 99 | 100 | // then 101 | XCTAssertNotNil(sut.constraints.first { $0.firstAttribute == .height && $0.constant == 10 }) 102 | XCTAssertNil(sut.constraints.first { $0.firstAttribute == .width }) 103 | } 104 | 105 | func testStackDistributedToEqual() { 106 | // given 107 | let sut = HStack(alignment: .center) { 108 | Text("🌺 Hello") 109 | .font(UIFont.systemFont(ofSize: 20, weight: .bold)) 110 | Text("🦄 HypeUI") 111 | .font(UIFont.systemFont(ofSize: 16, weight: .regular)) 112 | ScrollView(.vertical) { 113 | Text("Scrolling...") 114 | .font(UIFont.systemFont(ofSize: 14, weight: .heavy)) 115 | } 116 | } 117 | 118 | // when 119 | contentView.addSubviewWithFit( 120 | HStack(alignment: .center) { 121 | VStack(alignment: .center) { 122 | sut.distributed(.fillEqually) 123 | } 124 | } 125 | ) 126 | sut.layoutIfNeeded() 127 | 128 | // then 129 | XCTAssertEqual(Set(sut.subviews.map { $0.bounds.width }).count, 1) 130 | XCTAssertEqual(Set(sut.subviews.map { $0.bounds.height }).count, 3) 131 | } 132 | 133 | func testOverlayAlignmentCenter() { 134 | // given 135 | let sut = Text() 136 | .frame(width: 100, height: 100) 137 | 138 | // when 139 | contentView.addSubviewWithFit( 140 | Text() 141 | .frame(width: 500, height: 500) 142 | .overlay(alignment: .center, view: sut) 143 | ) 144 | contentView.layoutIfNeeded() 145 | 146 | // then 147 | XCTAssertEqual(contentView.center, contentView.convert(CGPoint(x: sut.bounds.midX, y: sut.bounds.midY), from: sut)) 148 | } 149 | 150 | func testOverlayAlignmentTopLeading() { 151 | // given 152 | let sut = Text() 153 | .frame(width: 100, height: 100) 154 | 155 | // when 156 | contentView.addSubviewWithFit( 157 | Text() 158 | .frame(width: 500, height: 500) 159 | .overlay(alignment: .topLeading, view: sut) 160 | ) 161 | contentView.layoutIfNeeded() 162 | 163 | // then 164 | XCTAssertEqual(CGRect(x: 0, y: 0, width: 100, height: 100), contentView.convert(sut.bounds, from: sut)) 165 | } 166 | 167 | func testOverlayAlignmentBottomTrailing() { 168 | // given 169 | let sut = Text() 170 | .frame(width: 100, height: 100) 171 | 172 | // when 173 | contentView.addSubviewWithFit( 174 | Text() 175 | .frame(width: 500, height: 500) 176 | .overlay(alignment: .bottomTrailing, view: sut) 177 | ) 178 | contentView.layoutIfNeeded() 179 | 180 | // then 181 | let output = contentView.convert(sut.bounds, from: sut) 182 | XCTAssertEqual(contentView.frame.maxX - 100, output.minX) 183 | XCTAssertEqual(contentView.frame.maxY - 100, output.minY) 184 | XCTAssertEqual(CGSize(width: 100, height: 100), output.size) 185 | } 186 | 187 | func testCenter() { 188 | // Given 189 | let sut = Text() 190 | .frame(width: 200, height: 200) 191 | 192 | // When 193 | contentView.addSubviewWithFit( 194 | ZStack { 195 | UIView() 196 | .frame(width: 400, height: 400) 197 | sut.center() 198 | }.center() 199 | ) 200 | contentView.layoutIfNeeded() 201 | 202 | // Then 203 | XCTAssertEqual(sut.center, CGPoint(x: 200, y: 200)) 204 | XCTAssertEqual(sut.bounds.size, CGSize(width: 200, height: 200)) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Tests/HypeUITests/XCLayoutTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | // MARK: - XCLayoutTestCase 20 | 21 | @testable import HypeUI 22 | class XCLayoutTestCase: XCTestCase { 23 | var contentView: UIView! 24 | 25 | override func setUpWithError() throws { 26 | let viewController = UIViewController() 27 | let window = UIWindow() 28 | window.addSubview(viewController.view) 29 | window.makeKeyAndVisible() 30 | contentView = UIView() 31 | viewController.view.addSubviewWithFit(contentView) 32 | RunLoop.current.run(until: Date()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/HypeUITests/ZStackTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 Hyperconnect Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | // MARK: - ZStackTests 20 | 21 | @testable import HypeUI 22 | final class ZStackTests: XCLayoutTestCase { 23 | 24 | func testZStack() { 25 | // given 26 | let image = Image() 27 | let text = Text("*****") 28 | let stack = VStack { 29 | Spacer() 30 | Spacer() 31 | Spacer() 32 | } 33 | 34 | // when 35 | let sut = ZStack { 36 | image 37 | text 38 | stack 39 | } 40 | contentView.addSubviewWithFit(sut) 41 | contentView.layoutIfNeeded() 42 | 43 | XCTAssertEqual(image.frame, text.frame) 44 | XCTAssertEqual(image.frame, stack.frame) 45 | } 46 | 47 | func testZStackWithVStack() { 48 | // given 49 | let spacer = Spacer() 50 | 51 | // when 52 | let sut = ZStack { 53 | VStack(spacing: 10) { 54 | Image() 55 | .frame(width: 20, height: 20) 56 | Text("*****") 57 | .frame(height: 20) 58 | spacer 59 | } 60 | } 61 | contentView.addSubviewWithFit(sut) 62 | contentView.layoutIfNeeded() 63 | 64 | // then 65 | XCTAssertEqual(spacer.frame.minY, 60) // 20 + 10 + 20 + 10 66 | XCTAssertEqual(spacer.frame.minX, 0) // Distribution.fill 67 | } 68 | } 69 | --------------------------------------------------------------------------------