├── .github ├── FUNDING.yml └── workflows │ └── swift.yml ├── .gitignore ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── LoginExample ├── LoginStevia.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── LoginStevia │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── Info.plist │ ├── LoginVC.swift │ ├── LoginViewNative.swift │ └── LoginViewStevia.swift ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── PrivacyInfo.xcprivacy └── Stevia │ ├── Stevia+Alignment.swift │ ├── Stevia+Baselines.swift │ ├── Stevia+Center.swift │ ├── Stevia+Constraints.swift │ ├── Stevia+Content.swift │ ├── Stevia+DoubleDash.swift │ ├── Stevia+Equation.swift │ ├── Stevia+Fill.swift │ ├── Stevia+FlexibleMargin.swift │ ├── Stevia+GetConstraint.swift │ ├── Stevia+Hierarchy.swift │ ├── Stevia+LayoutAnchors.swift │ ├── Stevia+Notifications.swift │ ├── Stevia+Operators.swift │ ├── Stevia+Percentage.swift │ ├── Stevia+Position.swift │ ├── Stevia+Size.swift │ ├── Stevia+Stacks.swift │ └── Stevia+Style.swift ├── SteviaLayout.podspec ├── Tests └── SteviaTests │ ├── BaselineTests.swift │ ├── CenterTests.swift │ ├── ContentTests.swift │ ├── EquationTests.swift │ ├── FillTests.swift │ ├── FlexibleMarginTests.swift │ ├── FullLayoutTests.swift │ ├── GetConstraintsTests.swift │ ├── HierarchyTests.swift │ ├── LayoutTests.swift │ ├── PositionTests.swift │ ├── SizeTests.swift │ └── StyleTests.swift └── banner.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: freshos 2 | github: s4cha 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: macos-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: swift build -v 21 | - name: Run tests 22 | run: swift test -v 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | #build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # CocoaPods 21 | # 22 | # We recommend against adding the Pods directory to your .gitignore. However 23 | # you should judge for yourself, the pros and cons are mentioned at: 24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 25 | # 26 | # Pods/ 27 | 28 | # Carthage 29 | # 30 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 31 | # Carthage/Checkouts 32 | Stevia/LoginExample/Carthage/Checkouts 33 | Stevia/LoginExample/iOSInjectionProject/ 34 | 35 | .DS_Store 36 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | - cyclomatic_complexity 4 | - variable_name 5 | - function_body_length 6 | - type_body_length 7 | - valid_docs 8 | opt_in_rules: 9 | - empty_count 10 | # - missing_docs 11 | # - force_unwrapping 12 | 13 | excluded: 14 | - LoginExample 15 | - SteviaTests 16 | - Playground 17 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | Put unreleased changes here 9 | - Preprocessor flag `#if canImport(UIKit)` moved to wrap all UIKit related types. 10 | 11 | ## [4.6.0] - 2019-09-30 12 | ### Changed 13 | - Adds support for Xcode11 & Swift 5.1 14 | 15 | ## [4.4.4] - 2019-01-03 16 | ### Changed 17 | `fillContainer` now returns self to make it chainable 18 | 19 | ## [4.4.3] - 2019-01-03 20 | ### Changed 21 | Fixes Equation api >= broken operator 22 | 23 | ## [4.4.2] - 2019-01-03 24 | ### Changed 25 | Fixes view.bottom/top/right/leftConstraint possibly returning wrong constraint if the one you want is not there yet. (looking for constraint in the view itself after looking for it in the superview) 26 | The fix makes sure it only looks for the constraint in the view itself for width and height constraints, that corresponds to constraints added via width/heightAnchors api. 27 | 28 | ## [4.4.1] - 2018-12-28 29 | ### Changed 30 | - Make sure userAddedConstraints doesn't return layout margins. Fixes #104 Kudos @mpsnp 👏 31 | - Code clean 32 | - Removing deprecated tap helper (discussion here #42) alternatives : https://github.com/XCEssentials/ViewEvents 33 | 34 | ## [4.4.0] - 2018-09-18 35 | ### Changed 36 | - Add support for Xcode10 & Swift 4.2 37 | 38 | ## [4.3.2] - 2018-09-09 39 | ### Added 40 | - Adds support for baselines, kudos to [@bellebethcooper](https://github.com/bellebethcooper) for bringing this up 🚀 41 | 42 | ```swift 43 | align(lastBaselines: label, label2, label3) 44 | label.FirstBaseline == label.LastBaseline 45 | ``` 46 | 47 | ### Changed 48 | - Fixes "view.heightConstraint being nil" when set from IB or anchors. Thanks [@leidi0129](https://github.com/leidi0129) for spotting the issue 👏 49 | 50 | ## [4.3.0] - 2018-04-04 51 | ### Changed 52 | - Supports Xcode 9.3 & Swift 4.1 53 | 54 | ## [4.2.0] - 2018-01-09 55 | 56 | ### Changed 57 | - `==` operator now works even if views are not at the same level in the view hierarchy 58 | - `==` operator is now reflexive 59 | - Re-opening Layout([array]) overload Some needed it for dynamic layout so it's back :) 60 | 61 | 62 | 63 | ## [4.1.0] - 2017-11-20 64 | 65 | ### Added 66 | - UILayoutSupport & UILayoutGuide are now supported in Equation base api. 67 | 68 | ## [4.1.0] - 2017-09-29 69 | 70 | ### Changed 71 | - Supports Xcode 9 & Swift 4 72 | 73 | ## [3.2.0] - 2017-06-14 74 | 75 | ### Changed 76 | - Size constraints such as width and height can now be added without the need for a superview. 77 | 78 | ### Added 79 | - New leadingConstraint & trailingConstraint property 80 | 81 | - Kudos to [@trupin](https://github.com/trupin) and [@cowgp](https://github.com/cowgp) for this release 🎉 82 | 83 | ## [3.1.4] - 2017-06-14 84 | 85 | ### Added 86 | - tvOS Support 87 | 88 | ## [3.1.4] - 2017-06-14 89 | 90 | ### Changed 91 | - Built with Xcode 8.3.1 92 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at sachadso@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /LoginExample/LoginStevia.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 99E2AA8D23BF7C4E00CBD63C /* Stevia in Frameworks */ = {isa = PBXBuildFile; productRef = 99E2AA8C23BF7C4E00CBD63C /* Stevia */; }; 11 | 99FC80DF1BBD147B0068503C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99FC80DE1BBD147B0068503C /* AppDelegate.swift */; }; 12 | 99FC80E11BBD147B0068503C /* LoginVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99FC80E01BBD147B0068503C /* LoginVC.swift */; }; 13 | 99FC80E61BBD147B0068503C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 99FC80E51BBD147B0068503C /* Assets.xcassets */; }; 14 | 99FC80E91BBD147B0068503C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 99FC80E71BBD147B0068503C /* LaunchScreen.storyboard */; }; 15 | 99FC80F31BBD14BB0068503C /* LoginViewNative.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99FC80F21BBD14BB0068503C /* LoginViewNative.swift */; }; 16 | 99FC80FA1BBD16840068503C /* LoginViewStevia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99FC80F91BBD16840068503C /* LoginViewStevia.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | E27AF8B22247476800BD88F8 /* Embed Frameworks */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = ""; 24 | dstSubfolderSpec = 10; 25 | files = ( 26 | ); 27 | name = "Embed Frameworks"; 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXCopyFilesBuildPhase section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 99E2AA8A23BF7C3E00CBD63C /* Stevia */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Stevia; path = ..; sourceTree = ""; }; 34 | 99FC80DB1BBD147B0068503C /* LoginStevia.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LoginStevia.app; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | 99FC80DE1BBD147B0068503C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 36 | 99FC80E01BBD147B0068503C /* LoginVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginVC.swift; sourceTree = ""; }; 37 | 99FC80E51BBD147B0068503C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 38 | 99FC80E81BBD147B0068503C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 39 | 99FC80EA1BBD147B0068503C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 99FC80F21BBD14BB0068503C /* LoginViewNative.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewNative.swift; sourceTree = ""; }; 41 | 99FC80F91BBD16840068503C /* LoginViewStevia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewStevia.swift; sourceTree = ""; }; 42 | /* End PBXFileReference section */ 43 | 44 | /* Begin PBXFrameworksBuildPhase section */ 45 | 99FC80D81BBD147B0068503C /* Frameworks */ = { 46 | isa = PBXFrameworksBuildPhase; 47 | buildActionMask = 2147483647; 48 | files = ( 49 | 99E2AA8D23BF7C4E00CBD63C /* Stevia in Frameworks */, 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXFrameworksBuildPhase section */ 54 | 55 | /* Begin PBXGroup section */ 56 | 99E2AA8B23BF7C4E00CBD63C /* Frameworks */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | ); 60 | name = Frameworks; 61 | sourceTree = ""; 62 | }; 63 | 99FC80D21BBD147B0068503C = { 64 | isa = PBXGroup; 65 | children = ( 66 | 99E2AA8A23BF7C3E00CBD63C /* Stevia */, 67 | 99FC80DD1BBD147B0068503C /* LoginStevia */, 68 | 99FC80DC1BBD147B0068503C /* Products */, 69 | 99E2AA8B23BF7C4E00CBD63C /* Frameworks */, 70 | ); 71 | sourceTree = ""; 72 | }; 73 | 99FC80DC1BBD147B0068503C /* Products */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 99FC80DB1BBD147B0068503C /* LoginStevia.app */, 77 | ); 78 | name = Products; 79 | sourceTree = ""; 80 | }; 81 | 99FC80DD1BBD147B0068503C /* LoginStevia */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 99FC80DE1BBD147B0068503C /* AppDelegate.swift */, 85 | 99FC80E01BBD147B0068503C /* LoginVC.swift */, 86 | 99FC80F21BBD14BB0068503C /* LoginViewNative.swift */, 87 | 99FC80F91BBD16840068503C /* LoginViewStevia.swift */, 88 | 99FC80E51BBD147B0068503C /* Assets.xcassets */, 89 | 99FC80E71BBD147B0068503C /* LaunchScreen.storyboard */, 90 | 99FC80EA1BBD147B0068503C /* Info.plist */, 91 | ); 92 | path = LoginStevia; 93 | sourceTree = ""; 94 | }; 95 | /* End PBXGroup section */ 96 | 97 | /* Begin PBXNativeTarget section */ 98 | 99FC80DA1BBD147B0068503C /* LoginStevia */ = { 99 | isa = PBXNativeTarget; 100 | buildConfigurationList = 99FC80ED1BBD147B0068503C /* Build configuration list for PBXNativeTarget "LoginStevia" */; 101 | buildPhases = ( 102 | 99FC80D71BBD147B0068503C /* Sources */, 103 | 99FC80D81BBD147B0068503C /* Frameworks */, 104 | 99FC80D91BBD147B0068503C /* Resources */, 105 | E27AF8B22247476800BD88F8 /* Embed Frameworks */, 106 | ); 107 | buildRules = ( 108 | ); 109 | dependencies = ( 110 | ); 111 | name = LoginStevia; 112 | packageProductDependencies = ( 113 | 99E2AA8C23BF7C4E00CBD63C /* Stevia */, 114 | ); 115 | productName = LoginNadir; 116 | productReference = 99FC80DB1BBD147B0068503C /* LoginStevia.app */; 117 | productType = "com.apple.product-type.application"; 118 | }; 119 | /* End PBXNativeTarget section */ 120 | 121 | /* Begin PBXProject section */ 122 | 99FC80D31BBD147B0068503C /* Project object */ = { 123 | isa = PBXProject; 124 | attributes = { 125 | LastUpgradeCheck = 1100; 126 | ORGANIZATIONNAME = "Sacha Durand Saint Omer"; 127 | TargetAttributes = { 128 | 99FC80DA1BBD147B0068503C = { 129 | CreatedOnToolsVersion = 7.0; 130 | LastSwiftMigration = 1100; 131 | }; 132 | }; 133 | }; 134 | buildConfigurationList = 99FC80D61BBD147B0068503C /* Build configuration list for PBXProject "LoginStevia" */; 135 | compatibilityVersion = "Xcode 3.2"; 136 | developmentRegion = en; 137 | hasScannedForEncodings = 0; 138 | knownRegions = ( 139 | en, 140 | Base, 141 | ); 142 | mainGroup = 99FC80D21BBD147B0068503C; 143 | productRefGroup = 99FC80DC1BBD147B0068503C /* Products */; 144 | projectDirPath = ""; 145 | projectRoot = ""; 146 | targets = ( 147 | 99FC80DA1BBD147B0068503C /* LoginStevia */, 148 | ); 149 | }; 150 | /* End PBXProject section */ 151 | 152 | /* Begin PBXResourcesBuildPhase section */ 153 | 99FC80D91BBD147B0068503C /* Resources */ = { 154 | isa = PBXResourcesBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | 99FC80E91BBD147B0068503C /* LaunchScreen.storyboard in Resources */, 158 | 99FC80E61BBD147B0068503C /* Assets.xcassets in Resources */, 159 | ); 160 | runOnlyForDeploymentPostprocessing = 0; 161 | }; 162 | /* End PBXResourcesBuildPhase section */ 163 | 164 | /* Begin PBXSourcesBuildPhase section */ 165 | 99FC80D71BBD147B0068503C /* Sources */ = { 166 | isa = PBXSourcesBuildPhase; 167 | buildActionMask = 2147483647; 168 | files = ( 169 | 99FC80F31BBD14BB0068503C /* LoginViewNative.swift in Sources */, 170 | 99FC80FA1BBD16840068503C /* LoginViewStevia.swift in Sources */, 171 | 99FC80E11BBD147B0068503C /* LoginVC.swift in Sources */, 172 | 99FC80DF1BBD147B0068503C /* AppDelegate.swift in Sources */, 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | }; 176 | /* End PBXSourcesBuildPhase section */ 177 | 178 | /* Begin PBXVariantGroup section */ 179 | 99FC80E71BBD147B0068503C /* LaunchScreen.storyboard */ = { 180 | isa = PBXVariantGroup; 181 | children = ( 182 | 99FC80E81BBD147B0068503C /* Base */, 183 | ); 184 | name = LaunchScreen.storyboard; 185 | sourceTree = ""; 186 | }; 187 | /* End PBXVariantGroup section */ 188 | 189 | /* Begin XCBuildConfiguration section */ 190 | 99FC80EB1BBD147B0068503C /* Debug */ = { 191 | isa = XCBuildConfiguration; 192 | buildSettings = { 193 | ALWAYS_SEARCH_USER_PATHS = NO; 194 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 195 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 196 | CLANG_CXX_LIBRARY = "libc++"; 197 | CLANG_ENABLE_MODULES = YES; 198 | CLANG_ENABLE_OBJC_ARC = YES; 199 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 200 | CLANG_WARN_BOOL_CONVERSION = YES; 201 | CLANG_WARN_COMMA = YES; 202 | CLANG_WARN_CONSTANT_CONVERSION = YES; 203 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 204 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 205 | CLANG_WARN_EMPTY_BODY = YES; 206 | CLANG_WARN_ENUM_CONVERSION = YES; 207 | CLANG_WARN_INFINITE_RECURSION = YES; 208 | CLANG_WARN_INT_CONVERSION = YES; 209 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 210 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 211 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 212 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 213 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 214 | CLANG_WARN_STRICT_PROTOTYPES = YES; 215 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 216 | CLANG_WARN_UNREACHABLE_CODE = YES; 217 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 218 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 219 | COPY_PHASE_STRIP = NO; 220 | DEBUG_INFORMATION_FORMAT = dwarf; 221 | ENABLE_STRICT_OBJC_MSGSEND = YES; 222 | ENABLE_TESTABILITY = YES; 223 | GCC_C_LANGUAGE_STANDARD = gnu99; 224 | GCC_DYNAMIC_NO_PIC = NO; 225 | GCC_NO_COMMON_BLOCKS = YES; 226 | GCC_OPTIMIZATION_LEVEL = 0; 227 | GCC_PREPROCESSOR_DEFINITIONS = ( 228 | "DEBUG=1", 229 | "$(inherited)", 230 | ); 231 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 232 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 233 | GCC_WARN_UNDECLARED_SELECTOR = YES; 234 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 235 | GCC_WARN_UNUSED_FUNCTION = YES; 236 | GCC_WARN_UNUSED_VARIABLE = YES; 237 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 238 | MTL_ENABLE_DEBUG_INFO = YES; 239 | ONLY_ACTIVE_ARCH = YES; 240 | SDKROOT = iphoneos; 241 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 242 | }; 243 | name = Debug; 244 | }; 245 | 99FC80EC1BBD147B0068503C /* Release */ = { 246 | isa = XCBuildConfiguration; 247 | buildSettings = { 248 | ALWAYS_SEARCH_USER_PATHS = NO; 249 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 250 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 251 | CLANG_CXX_LIBRARY = "libc++"; 252 | CLANG_ENABLE_MODULES = YES; 253 | CLANG_ENABLE_OBJC_ARC = YES; 254 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 255 | CLANG_WARN_BOOL_CONVERSION = YES; 256 | CLANG_WARN_COMMA = YES; 257 | CLANG_WARN_CONSTANT_CONVERSION = YES; 258 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 259 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 260 | CLANG_WARN_EMPTY_BODY = YES; 261 | CLANG_WARN_ENUM_CONVERSION = YES; 262 | CLANG_WARN_INFINITE_RECURSION = YES; 263 | CLANG_WARN_INT_CONVERSION = YES; 264 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 265 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 266 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 267 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 268 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 269 | CLANG_WARN_STRICT_PROTOTYPES = YES; 270 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 271 | CLANG_WARN_UNREACHABLE_CODE = YES; 272 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 273 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 274 | COPY_PHASE_STRIP = NO; 275 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 276 | ENABLE_NS_ASSERTIONS = NO; 277 | ENABLE_STRICT_OBJC_MSGSEND = YES; 278 | GCC_C_LANGUAGE_STANDARD = gnu99; 279 | GCC_NO_COMMON_BLOCKS = YES; 280 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 281 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 282 | GCC_WARN_UNDECLARED_SELECTOR = YES; 283 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 284 | GCC_WARN_UNUSED_FUNCTION = YES; 285 | GCC_WARN_UNUSED_VARIABLE = YES; 286 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 287 | MTL_ENABLE_DEBUG_INFO = NO; 288 | SDKROOT = iphoneos; 289 | SWIFT_COMPILATION_MODE = wholemodule; 290 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 291 | VALIDATE_PRODUCT = YES; 292 | }; 293 | name = Release; 294 | }; 295 | 99FC80EE1BBD147B0068503C /* Debug */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 299 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 300 | FRAMEWORK_SEARCH_PATHS = "$(inherited)"; 301 | INFOPLIST_FILE = LoginStevia/Info.plist; 302 | LD_RUNPATH_SEARCH_PATHS = ( 303 | "$(inherited)", 304 | "@executable_path/Frameworks", 305 | ); 306 | PRODUCT_BUNDLE_IDENTIFIER = com.octopepper.LoginNadir; 307 | PRODUCT_NAME = LoginStevia; 308 | SWIFT_VERSION = 5.0; 309 | }; 310 | name = Debug; 311 | }; 312 | 99FC80EF1BBD147B0068503C /* Release */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 316 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 317 | FRAMEWORK_SEARCH_PATHS = "$(inherited)"; 318 | INFOPLIST_FILE = LoginStevia/Info.plist; 319 | LD_RUNPATH_SEARCH_PATHS = ( 320 | "$(inherited)", 321 | "@executable_path/Frameworks", 322 | ); 323 | PRODUCT_BUNDLE_IDENTIFIER = com.octopepper.LoginNadir; 324 | PRODUCT_NAME = LoginStevia; 325 | SWIFT_VERSION = 5.0; 326 | }; 327 | name = Release; 328 | }; 329 | /* End XCBuildConfiguration section */ 330 | 331 | /* Begin XCConfigurationList section */ 332 | 99FC80D61BBD147B0068503C /* Build configuration list for PBXProject "LoginStevia" */ = { 333 | isa = XCConfigurationList; 334 | buildConfigurations = ( 335 | 99FC80EB1BBD147B0068503C /* Debug */, 336 | 99FC80EC1BBD147B0068503C /* Release */, 337 | ); 338 | defaultConfigurationIsVisible = 0; 339 | defaultConfigurationName = Release; 340 | }; 341 | 99FC80ED1BBD147B0068503C /* Build configuration list for PBXNativeTarget "LoginStevia" */ = { 342 | isa = XCConfigurationList; 343 | buildConfigurations = ( 344 | 99FC80EE1BBD147B0068503C /* Debug */, 345 | 99FC80EF1BBD147B0068503C /* Release */, 346 | ); 347 | defaultConfigurationIsVisible = 0; 348 | defaultConfigurationName = Release; 349 | }; 350 | /* End XCConfigurationList section */ 351 | 352 | /* Begin XCSwiftPackageProductDependency section */ 353 | 99E2AA8C23BF7C4E00CBD63C /* Stevia */ = { 354 | isa = XCSwiftPackageProductDependency; 355 | productName = Stevia; 356 | }; 357 | /* End XCSwiftPackageProductDependency section */ 358 | }; 359 | rootObject = 99FC80D31BBD147B0068503C /* Project object */; 360 | } 361 | -------------------------------------------------------------------------------- /LoginExample/LoginStevia.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LoginExample/LoginStevia.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LoginExample/LoginStevia/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | 2 | // AppDelegate.swift 3 | // LoginStevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 01/10/15. 6 | // Copyright © 2015 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection10.bundle")?.load() 18 | window = UIWindow(frame: UIScreen.main.bounds) 19 | window?.rootViewController = LoginVC() 20 | window?.makeKeyAndVisible() 21 | return true 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /LoginExample/LoginStevia/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /LoginExample/LoginStevia/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 | 27 | 28 | -------------------------------------------------------------------------------- /LoginExample/LoginStevia/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /LoginExample/LoginStevia/LoginVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // LoginStevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 01/10/15. 6 | // Copyright © 2015 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LoginVC: UIViewController { 12 | 13 | var v: LoginViewStevia! 14 | override func loadView() { 15 | v = LoginViewStevia() 16 | view = v 17 | } 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | // Link actions 23 | v.login.addTarget(self, action: #selector(login), for: .touchUpInside) 24 | 25 | // Here we want to reload the view after injection 26 | // this is only needed for live reload 27 | on("INJECTION_BUNDLE_NOTIFICATION") { 28 | self.loadView() 29 | } 30 | } 31 | 32 | @objc 33 | func login() { 34 | print("login tapped") 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /LoginExample/LoginStevia/LoginViewNative.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewNative.swift 3 | // LoginStevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 01/10/15. 6 | // Copyright © 2015 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LoginViewNative: UIView { 12 | 13 | let email = UITextField() 14 | let password = UITextField() 15 | let login = UIButton() 16 | 17 | convenience init() { 18 | self.init(frame:CGRect.zero) 19 | 20 | // 01 - View Hieararchy 21 | email.translatesAutoresizingMaskIntoConstraints = false 22 | password.translatesAutoresizingMaskIntoConstraints = false 23 | login.translatesAutoresizingMaskIntoConstraints = false 24 | addSubview(email) 25 | addSubview(password) 26 | addSubview(login) 27 | 28 | // 02 - Layout (using latest layoutAnchors) 29 | email.topAnchor.constraint(equalTo: topAnchor, constant: 100).isActive = true 30 | email.leftAnchor.constraint(equalTo: leftAnchor, constant: 8).isActive = true 31 | email.rightAnchor.constraint(equalTo: rightAnchor, constant: -8).isActive = true 32 | email.heightAnchor.constraint(equalToConstant: 80).isActive = true 33 | 34 | password.topAnchor.constraint(equalTo: email.bottomAnchor, constant: 8).isActive = true 35 | password.leftAnchor.constraint(equalTo: leftAnchor, constant: 8).isActive = true 36 | password.rightAnchor.constraint(equalTo: rightAnchor, constant: -8).isActive = true 37 | password.heightAnchor.constraint(equalToConstant: 80).isActive = true 38 | 39 | 40 | login.topAnchor.constraint(lessThanOrEqualTo: password.bottomAnchor, constant: 20).isActive = true 41 | login.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0).isActive = true 42 | login.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 43 | login.rightAnchor.constraint(equalTo: rightAnchor).isActive = true 44 | login.heightAnchor.constraint(equalToConstant: 80).isActive = true 45 | 46 | // 03 - Styling 47 | backgroundColor = .gray 48 | email.borderStyle = .roundedRect 49 | email.autocorrectionType = .no 50 | email.keyboardType = .emailAddress 51 | email.font = UIFont(name: "HelveticaNeue-Light", size: 26) 52 | email.returnKeyType = .next 53 | password.borderStyle = .roundedRect 54 | password.font = UIFont(name: "HelveticaNeue-Light", size: 26) 55 | password.isSecureTextEntry = true 56 | password.returnKeyType = .done 57 | login.backgroundColor = .lightGray 58 | 59 | // 04 - Content 60 | email.placeholder = "Email" 61 | password.placeholder = "Password" 62 | login.setTitle("Login", for: .normal) 63 | } 64 | 65 | func loginTapped() { 66 | //Do something 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /LoginExample/LoginStevia/LoginViewStevia.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewStevia.swift 3 | // LoginStevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 01/10/15. 6 | // Copyright © 2015 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Stevia 11 | 12 | class LoginViewStevia: UIView { 13 | 14 | let email = UITextField() 15 | let password = UITextField() 16 | let login = UIButton() 17 | 18 | convenience init() { 19 | self.init(frame:CGRect.zero) 20 | // Get injectionForXcode here : http://johnholdsworth.com/injection.html 21 | 22 | // 01 -View Hierarchy 23 | // This essentially does `translatesAutoresizingMaskIntoConstraints = false` 24 | // and `addSubsview()`. The neat benefit is that 25 | // (`Subviews` calls can be nested which will visually show hierarchy ! ) 26 | subviews { 27 | email 28 | password 29 | login 30 | } 31 | 32 | // 02 - Vertical + Horizontal Layout in one pass 33 | // With type-safe visual format 34 | layout { 35 | 100 36 | |-email-| ~ 80 37 | 8 38 | |-password-| ~ 80 39 | >=20 40 | |login| ~ 80 41 | 0 42 | } 43 | 44 | // ⛓ Chainable api 45 | // email.top(100).fillHorizontally(m: 8).height(80) 46 | // password.Top == email.Bottom + 8 47 | // password.fillHorizontally(m: 8).height(80) 48 | // login.bottom(0).fillHorizontally().height(80) 49 | 50 | // 📐 Equation based layout (Try it out!) 51 | //This comes in handy to cover tricky layout cases 52 | // email.Top == Top + 100 53 | // email.Left == Left + 8 54 | // email.Right == Right - 8 55 | // email.Height == 80 56 | // 57 | // password.Top == email.Bottom + 8 58 | // password.Left == Left + 8 59 | // password.Right == Right - 8 60 | // password.Height == 80 61 | // 62 | // password.Top == email.Bottom + 8 63 | // password.Left == Left + 8 64 | // password.Right == Right - 8 65 | // password.Height == 80 66 | // 67 | // login.Left == Left 68 | // login.Right == Right 69 | // login.Bottom == Bottom 70 | // login.Height == 80 71 | 72 | 73 | // 03 - Styling 🎨 74 | backgroundColor = .gray 75 | email.style(commonFieldStyle) 76 | password.style(commonFieldStyle).style { f in 77 | f.isSecureTextEntry = true 78 | f.returnKeyType = .done 79 | } 80 | login.backgroundColor = .lightGray 81 | 82 | // 04 - Content 🖋 83 | email.placeholder = "Email" 84 | password.placeholder = "Password" 85 | login.setTitle("Login", for: .normal) 86 | } 87 | 88 | // Style can be extracted and applied kind of like css \o/ 89 | // but in pure Swift though! 90 | func commonFieldStyle(_ f:UITextField) { 91 | f.borderStyle = .roundedRect 92 | f.font = UIFont(name: "HelveticaNeue-Light", size: 26) 93 | f.returnKeyType = .next 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "0d1e08a917a619df6510ae568b5ab623f1fc22dffa644b8c590c04c4a5b086bc", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-numerics", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-numerics", 8 | "state" : { 9 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", 10 | "version" : "1.0.2" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Stevia", 7 | platforms: [ 8 | .iOS(.v12), 9 | .tvOS(.v12) 10 | ], 11 | products: [.library(name: "Stevia", targets: ["Stevia"])], 12 | dependencies: [ 13 | .package(url: "https://github.com/apple/swift-numerics", from: "1.0.2"), 14 | ], 15 | targets: [ 16 | .target(name: "Stevia", path: "Sources", resources: [.copy("PrivacyInfo.xcprivacy")]), 17 | .testTarget(name: "SteviaTests", dependencies: [ 18 | "Stevia", 19 | .product(name: "Numerics", package: "swift-numerics"), 20 | ]), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /Sources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyTrackingDomains 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Alignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Alignment.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 10/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | /** Aligns an array of views Horizontally (on the X Axis) 13 | 14 | Example Usage: 15 | ``` 16 | align(horizontally: label,button,arrow) 17 | ``` 18 | 19 | Can also be used directly on horizontal layouts since they return the array of views : 20 | ``` 21 | align(horizontally: |-image1-image2-image3-|) 22 | ``` 23 | 24 | - Returns: The array of views, enabling chaining, 25 | 26 | */ 27 | @discardableResult 28 | @MainActor public func align(horizontally views: UIView...) -> [UIView] { 29 | return align(horizontally: views) 30 | } 31 | 32 | @available(*, deprecated, renamed: "align(horizontally:)") 33 | @discardableResult 34 | @MainActor public func alignHorizontally(_ views: UIView...) -> [UIView] { 35 | return align(horizontally: views) 36 | } 37 | 38 | /** Aligns an array of views Horizontally (on the X Axis) 39 | 40 | Example Usage: 41 | ``` 42 | align(horizontally: label,button,arrow) 43 | ``` 44 | 45 | Ca also be used directly on horizontal layouts since they return the array of views : 46 | ``` 47 | align(horizontally: |-image1-image2-image3-|) 48 | ``` 49 | 50 | - Returns: The array of views, enabling chaining, 51 | 52 | */ 53 | @discardableResult 54 | @MainActor public func align(horizontally views: [UIView]) -> [UIView] { 55 | align(.horizontal, views: views) 56 | return views 57 | } 58 | 59 | @available(*, deprecated, renamed: "align(horizontally:)") 60 | @discardableResult 61 | @MainActor public func alignHorizontally(_ views: [UIView]) -> [UIView] { 62 | align(.horizontal, views: views) 63 | return views 64 | } 65 | 66 | /** Aligns an array of views Vertically (on the Y Axis) 67 | 68 | Example Usage: 69 | ``` 70 | align(vertically: label,field,button) 71 | ``` 72 | 73 | - Returns: The array of views, enabling chaining, 74 | 75 | */ 76 | @MainActor public func align(vertically views: UIView...) { 77 | align(vertically: views) 78 | } 79 | 80 | @available(*, deprecated, renamed: "align(vertically:)") 81 | @MainActor public func alignVertically(_ views: UIView...) { 82 | align(vertically: views) 83 | } 84 | 85 | /** Aligns an array of views Vertically (on the Y Axis) 86 | 87 | Example Usage: 88 | ``` 89 | align(vertically: label,field,button) 90 | ``` 91 | 92 | - Returns: The array of views, enabling chaining, 93 | 94 | */ 95 | @MainActor public func align(vertically views: [UIView]) { 96 | align(.vertical, views: views) 97 | } 98 | 99 | @available(*, deprecated, renamed: "align(vertically:)") 100 | @MainActor public func alignVertically(_ views: [UIView]) { 101 | align(.vertical, views: views) 102 | } 103 | 104 | /** Aligns the center of two views 105 | 106 | Example Usage: 107 | ``` 108 | alignCenter(button, with:image) 109 | ``` 110 | */ 111 | @MainActor public func alignCenter(_ v1: UIView, with v2: UIView) { 112 | alignHorizontally(v1, with: v2) 113 | alignVertically(v1, with: v2) 114 | } 115 | 116 | /** Aligns two views Horizontall (on the X Axis) 117 | 118 | Example Usage: 119 | ``` 120 | alignHorizontally(label, with:field) 121 | ``` 122 | 123 | */ 124 | @MainActor public func alignHorizontally(_ v1: UIView, with v2: UIView, offset: Double = 0) { 125 | align(.horizontal, v1: v1, with: v2, offset: offset) 126 | } 127 | 128 | /** Aligns two views Vertically (on the Y Axis) 129 | 130 | Example Usage: 131 | ``` 132 | alignVertically(label, with:field) 133 | ``` 134 | 135 | */ 136 | @MainActor public func alignVertically(_ v1: UIView, with v2: UIView, offset: Double = 0) { 137 | align(.vertical, v1: v1, with: v2, offset: offset) 138 | } 139 | 140 | @MainActor private func align(_ axis: NSLayoutConstraint.Axis, views: [UIView]) { 141 | for (i, v) in views.enumerated() where views.count > i+1 { 142 | let v2 = views[i+1] 143 | if axis == .horizontal { 144 | alignHorizontally(v, with: v2) 145 | } else { 146 | alignVertically(v, with: v2) 147 | } 148 | } 149 | } 150 | 151 | @MainActor func align(_ axis: NSLayoutConstraint.Axis, v1: UIView, with v2: UIView, offset: Double) { 152 | if let spv = v1.superview { 153 | let center: NSLayoutConstraint.Attribute = axis == .horizontal ? .centerY : .centerX 154 | let c = constraint(item: v1, attribute: center, toItem: v2, constant: offset) 155 | spv.addConstraint(c) 156 | } 157 | } 158 | 159 | // MARK: Align sides 160 | 161 | /** Aligns tops of an array of views 162 | 163 | Example Usage: 164 | ``` 165 | align(tops: label,button,arrow) 166 | ``` 167 | 168 | Ca also be used directly on horizontal layouts since they return the array of views : 169 | ``` 170 | align(tops: |-image1-image2-image3-|) 171 | ``` 172 | 173 | - Returns: The array of views, enabling chaining, 174 | 175 | */ 176 | @discardableResult 177 | @MainActor public func align(tops views: UIView...) -> [UIView] { 178 | return align(tops: views) 179 | } 180 | 181 | @available(*, deprecated, renamed: "align(tops:)") 182 | @discardableResult 183 | @MainActor public func alignTops(_ views: UIView...) -> [UIView] { 184 | return align(tops: views) 185 | } 186 | 187 | /** Aligns tops of an array of views 188 | 189 | Example Usage: 190 | ``` 191 | align(tops: label,button,arrow) 192 | ``` 193 | 194 | Ca also be used directly on horizontal layouts since they return the array of views : 195 | ``` 196 | align(tops: |-image1-image2-image3-|) 197 | ``` 198 | 199 | - Returns: The array of views, enabling chaining, 200 | 201 | */ 202 | @discardableResult 203 | @MainActor public func align(tops views: [UIView]) -> [UIView] { 204 | align(.top, views: views) 205 | return views 206 | } 207 | 208 | @available(*, deprecated, renamed: "align(tops:)") 209 | @discardableResult 210 | @MainActor public func alignTops(_ views: [UIView]) -> [UIView] { 211 | align(.top, views: views) 212 | return views 213 | } 214 | 215 | /** Aligns bottoms of an array of views 216 | 217 | Example Usage: 218 | ``` 219 | align(bottoms: label,button,arrow) 220 | ``` 221 | 222 | Ca also be used directly on horizontal layouts since they return the array of views : 223 | ``` 224 | align(bottoms: |-image1-image2-image3-|) 225 | ``` 226 | 227 | - Returns: The array of views, enabling chaining, 228 | 229 | */ 230 | @discardableResult 231 | @MainActor public func align(bottoms views: UIView...) -> [UIView] { 232 | return align(bottoms: views) 233 | } 234 | 235 | @available(*, deprecated, renamed: "align(bottoms:)") 236 | @discardableResult 237 | @MainActor public func alignBottoms(_ views: UIView...) -> [UIView] { 238 | return align(bottoms: views) 239 | } 240 | 241 | /** Aligns bottoms of an array of views 242 | 243 | Example Usage: 244 | ``` 245 | align(bottoms: label,button,arrow) 246 | ``` 247 | 248 | Ca also be used directly on horizontal layouts since they return the array of views : 249 | ``` 250 | align(bottoms: |-image1-image2-image3-|) 251 | ``` 252 | 253 | - Returns: The array of views, enabling chaining, 254 | 255 | */ 256 | @discardableResult 257 | @MainActor public func align(bottoms views: [UIView]) -> [UIView] { 258 | align(.bottom, views: views) 259 | return views 260 | } 261 | 262 | @available(*, deprecated, renamed: "align(bottoms:)") 263 | @discardableResult 264 | @MainActor public func alignBottoms(_ views: [UIView]) -> [UIView] { 265 | align(.bottom, views: views) 266 | return views 267 | } 268 | 269 | /** Aligns left sides of an array of views 270 | 271 | Example Usage: 272 | ``` 273 | align(lefts: label,field,button) 274 | ``` 275 | 276 | - Returns: The array of views, enabling chaining, 277 | 278 | */ 279 | @discardableResult 280 | @MainActor public func align(lefts views: UIView...) -> [UIView] { 281 | return align(lefts: views) 282 | } 283 | 284 | @available(*, deprecated, renamed: "align(lefts:)") 285 | @discardableResult 286 | @MainActor public func alignLefts(_ views: UIView...) -> [UIView] { 287 | return align(lefts: views) 288 | } 289 | 290 | /** Aligns left sides of an array of views 291 | 292 | Example Usage: 293 | ``` 294 | align(lefts: label,field,button) 295 | ``` 296 | 297 | - Returns: The array of views, enabling chaining, 298 | 299 | */ 300 | @discardableResult 301 | @MainActor public func align(lefts views: [UIView]) -> [UIView] { 302 | align(.left, views: views) 303 | return views 304 | } 305 | 306 | @available(*, deprecated, renamed: "align(lefts:)") 307 | @discardableResult 308 | @MainActor public func alignLefts(_ views: [UIView]) -> [UIView] { 309 | align(.left, views: views) 310 | return views 311 | } 312 | 313 | /** Aligns right sides of an array of views 314 | 315 | Example Usage: 316 | ``` 317 | align(rights: label,field,button) 318 | ``` 319 | 320 | - Returns: The array of views, enabling chaining, 321 | 322 | */ 323 | @discardableResult 324 | @MainActor public func align(rights views: UIView...) -> [UIView] { 325 | return align(rights: views) 326 | } 327 | 328 | @available(*, deprecated, renamed: "align(rights:)") 329 | @discardableResult 330 | @MainActor public func alignRights(_ views: UIView...) -> [UIView] { 331 | return align(rights: views) 332 | } 333 | 334 | /** Aligns right sides of an array of views 335 | 336 | Example Usage: 337 | ``` 338 | align(rights: label,field,button) 339 | ``` 340 | 341 | - Returns: The array of views, enabling chaining, 342 | 343 | */ 344 | @discardableResult 345 | @MainActor public func align(rights views: [UIView]) -> [UIView] { 346 | align(.right, views: views) 347 | return views 348 | } 349 | 350 | @available(*, deprecated, renamed: "align(rights:)") 351 | @discardableResult 352 | @MainActor public func alignRights(_ views: [UIView]) -> [UIView] { 353 | align(.right, views: views) 354 | return views 355 | } 356 | 357 | /** Aligns leading sides of an array of views 358 | 359 | Example Usage: 360 | ``` 361 | align(leadings: [label,field,button]) 362 | ``` 363 | 364 | - Returns: The array of views, enabling chaining, 365 | 366 | */ 367 | @discardableResult 368 | @MainActor public func align(leadings views: [UIView]) -> [UIView] { 369 | align(.leading, views: views) 370 | return views 371 | } 372 | 373 | /** Aligns leading sides of an array of views 374 | 375 | Example Usage: 376 | ``` 377 | align(leadings: label,field,button) 378 | ``` 379 | 380 | - Returns: The array of views, enabling chaining, 381 | 382 | */ 383 | @discardableResult 384 | @MainActor public func align(leadings views: UIView...) -> [UIView] { 385 | align(.leading, views: views) 386 | return views 387 | } 388 | 389 | /** Aligns trailing sides of an array of views 390 | 391 | Example Usage: 392 | ``` 393 | align(trailing: [label,field,button]) 394 | ``` 395 | 396 | - Returns: The array of views, enabling chaining, 397 | 398 | */ 399 | @discardableResult 400 | @MainActor public func align(trailings views: [UIView]) -> [UIView] { 401 | align(.trailing, views: views) 402 | return views 403 | } 404 | 405 | /** Aligns trailing sides of an array of views 406 | 407 | Example Usage: 408 | ``` 409 | align(trailing: label,field,button) 410 | ``` 411 | 412 | - Returns: The array of views, enabling chaining, 413 | 414 | */ 415 | @discardableResult 416 | @MainActor public func align(trailings views: UIView...) -> [UIView] { 417 | align(.trailing, views: views) 418 | return views 419 | } 420 | 421 | @discardableResult 422 | @MainActor public func align(_ attribute: NSLayoutConstraint.Attribute, views: [UIView]) -> [UIView] { 423 | for (i, v) in views.enumerated() where views.count > i+1 { 424 | let v2 = views[i+1] 425 | if let spv = v.superview { 426 | let c = constraint(item: v, attribute: attribute, toItem: v2) 427 | spv.addConstraint(c) 428 | } 429 | } 430 | return views 431 | } 432 | #endif 433 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Baselines.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Baselines.swift 3 | // Stevia 4 | // 5 | // Created by Sacha on 09/09/2018. 6 | // Copyright © 2018 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | /** Aligns an array of views by their lastBaselines (on the Y Axis) 13 | 14 | Example Usage: 15 | ``` 16 | align(lastBaselines: label1, label2, label3) 17 | ``` 18 | 19 | Can also be used directly on horizontal layouts since they return the array of views : 20 | ``` 21 | align(lastBaselines: |-label1-label2-label3-|) 22 | ``` 23 | 24 | - Returns: The array of views, enabling chaining, 25 | 26 | */ 27 | @discardableResult 28 | @MainActor public func align(lastBaselines views: UIView...) -> [UIView] { 29 | return align(lastBaselines: views) 30 | } 31 | 32 | @discardableResult 33 | @MainActor public func align(lastBaselines views: [UIView]) -> [UIView] { 34 | for (i, v) in views.enumerated() where views.count > i+1 { 35 | let v2 = views[i+1] 36 | if #available(iOS 9.0, *) { 37 | v.lastBaselineAnchor.constraint(equalTo: v2.lastBaselineAnchor).isActive = true 38 | } else if let spv = v.superview { 39 | let c = constraint(item: v, attribute: .lastBaseline, toItem: v2) 40 | spv.addConstraint(c) 41 | } 42 | } 43 | return views 44 | } 45 | 46 | /** Aligns an array of views by their firstBaselines (on the Y Axis) 47 | 48 | Example Usage: 49 | ``` 50 | align(firstBaselines: label1, label2, label3) 51 | ``` 52 | 53 | Can also be used directly on horizontal layouts since they return the array of views : 54 | ``` 55 | align(firstBaselines: |-label1-label2-label3-|) 56 | ``` 57 | 58 | - Returns: The array of views, enabling chaining, 59 | 60 | */ 61 | @discardableResult 62 | @MainActor public func align(firstBaselines views: UIView...) -> [UIView] { 63 | return align(firstBaselines: views) 64 | } 65 | 66 | @discardableResult 67 | @MainActor public func align(firstBaselines views: [UIView]) -> [UIView] { 68 | for (i, v) in views.enumerated() where views.count > i+1 { 69 | let v2 = views[i+1] 70 | if #available(iOS 9.0, *) { 71 | v.firstBaselineAnchor.constraint(equalTo: v2.firstBaselineAnchor).isActive = true 72 | } else if let spv = v.superview { 73 | let c = constraint(item: v, attribute: .firstBaseline, toItem: v2) 74 | spv.addConstraint(c) 75 | } 76 | } 77 | return views 78 | } 79 | #endif 80 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Center.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Center.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 10/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public extension UIView { 13 | 14 | /** 15 | Centers the view in its container. 16 | 17 | ``` 18 | button.centerInContainer() 19 | ``` 20 | 21 | - Returns: Itself, enabling chaining, 22 | 23 | */ 24 | @discardableResult 25 | func centerInContainer() -> Self { 26 | if let spv = superview { 27 | alignCenter(self, with: spv) 28 | } 29 | return self 30 | } 31 | 32 | /** 33 | Centers the view horizontally (X axis) in its container. 34 | 35 | ``` 36 | button.centerHorizontally() 37 | button.centerHorizontally(offset: 40) 38 | ``` 39 | 40 | - Returns: Itself, enabling chaining, 41 | 42 | */ 43 | @discardableResult 44 | func centerHorizontally(offset: Double = 0) -> Self { 45 | if let spv = superview { 46 | align(.vertical, v1: self, with: spv, offset: offset) 47 | } 48 | return self 49 | } 50 | 51 | /** 52 | Centers the view horizontally (X axis) in its container. 53 | 54 | ``` 55 | button.centerHorizontally() 56 | button.centerHorizontally(offset: 40) 57 | ``` 58 | 59 | - Returns: Itself, enabling chaining, 60 | 61 | */ 62 | @discardableResult 63 | func centerHorizontally(offset: CGFloat) -> Self { 64 | centerHorizontally(offset: Double(offset)) 65 | } 66 | 67 | /** 68 | Centers the view horizontally (X axis) in its container. 69 | 70 | ``` 71 | button.centerHorizontally() 72 | button.centerHorizontally(offset: 40) 73 | ``` 74 | 75 | - Returns: Itself, enabling chaining, 76 | 77 | */ 78 | @discardableResult 79 | func centerHorizontally(offset: Int) -> Self { 80 | centerHorizontally(offset: Double(offset)) 81 | } 82 | 83 | /** 84 | Centers the view vertically (Y axis) in its container. 85 | 86 | ``` 87 | button.centerVertically() 88 | button.centerVertically(offset: 40) 89 | ``` 90 | 91 | - Returns: Itself, enabling chaining, 92 | 93 | */ 94 | @discardableResult 95 | func centerVertically(offset: Double = 0) -> Self { 96 | if let spv = superview { 97 | align(.horizontal, v1: self, with: spv, offset: offset) 98 | } 99 | return self 100 | } 101 | 102 | /** 103 | Centers the view vertically (Y axis) in its container. 104 | 105 | ``` 106 | button.centerVertically() 107 | button.centerVertically(offset: 40) 108 | ``` 109 | 110 | - Returns: Itself, enabling chaining, 111 | 112 | */ 113 | @discardableResult 114 | func centerVertically(offset: CGFloat) -> Self { 115 | centerVertically(offset: Double(offset)) 116 | } 117 | 118 | /** 119 | Centers the view vertically (Y axis) in its container. 120 | 121 | ``` 122 | button.centerVertically() 123 | button.centerVertically(offset: 40) 124 | ``` 125 | 126 | - Returns: Itself, enabling chaining, 127 | 128 | */ 129 | @discardableResult 130 | func centerVertically(offset: Int) -> Self { 131 | centerVertically(offset: Double(offset)) 132 | } 133 | } 134 | #endif 135 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Constraints.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Constraints.swift 3 | // LoginNadir 4 | // 5 | // Created by Sacha Durand Saint Omer on 01/10/15. 6 | // Copyright © 2015 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public class SteviaLayoutConstraint: NSLayoutConstraint { 13 | public static var defaultPriority: Float = UILayoutPriority.defaultHigh + 1 14 | } 15 | 16 | 17 | // MARK: - Shortcut 18 | 19 | public extension UIView { 20 | 21 | /** 22 | Helper for creating and adding NSLayoutConstraint but with default values provided. 23 | 24 | For instance 25 | 26 | addConstraint(item: view1, attribute: .CenterX, toItem: view2) 27 | 28 | is equivalent to 29 | 30 | addConstraint( 31 | NSLayoutConstraint(item: view1, 32 | attribute: .CenterX, 33 | relatedBy: .Equal, 34 | toItem: view2, 35 | attribute: .CenterX, 36 | multiplier: 1, 37 | constant: 0 38 | ) 39 | ) 40 | 41 | - Returns: The NSLayoutConstraint created. 42 | */ 43 | @discardableResult 44 | func addConstraint(item view1: AnyObject, 45 | attribute attr1: NSLayoutConstraint.Attribute, 46 | relatedBy: NSLayoutConstraint.Relation = .equal, 47 | toItem view2: AnyObject? = nil, 48 | attribute attr2: NSLayoutConstraint.Attribute? = nil, 49 | multiplier: Double = 1, 50 | constant: Double = 0) -> NSLayoutConstraint { 51 | let c = constraint( 52 | item: view1, attribute: attr1, 53 | relatedBy: relatedBy, 54 | toItem: view2, attribute: attr2, 55 | multiplier: multiplier, constant: constant) 56 | addConstraint(c) 57 | return c 58 | } 59 | } 60 | 61 | /** 62 | Helper for creating a NSLayoutConstraint but with default values provided. 63 | 64 | For instance 65 | 66 | constraint(item: view1, attribute: .CenterX, toItem: view2) 67 | 68 | is equivalent to 69 | 70 | NSLayoutConstraint(item: view1, attribute: .CenterX, 71 | relatedBy: .Equal, 72 | toItem: view2, attribute: .CenterX, 73 | multiplier: 1, constant: 0) 74 | 75 | - Returns: The NSLayoutConstraint created. 76 | */ 77 | @MainActor func constraint(item view1: AnyObject, 78 | attribute attr1: NSLayoutConstraint.Attribute, 79 | relatedBy: NSLayoutConstraint.Relation = .equal, 80 | toItem view2: AnyObject? = nil, 81 | attribute attr2: NSLayoutConstraint.Attribute? = nil, // Not an attribute?? 82 | multiplier: Double = 1, 83 | constant: Double = 0) -> NSLayoutConstraint { 84 | let c = SteviaLayoutConstraint(item: view1, attribute: attr1, 85 | relatedBy: relatedBy, 86 | toItem: view2, attribute: ((attr2 == nil) ? attr1 : attr2! ), 87 | multiplier: CGFloat(multiplier), constant: CGFloat(constant)) 88 | c.priority = UILayoutPriority(rawValue: SteviaLayoutConstraint.defaultPriority) 89 | return c 90 | } 91 | 92 | public extension UIView { 93 | 94 | /** 95 | Get User added constraints. For making complex changes on layout, we need to remove user added constraints. 96 | 97 | If we remove all constraints, it may return broken layout. 98 | 99 | Use this method as: 100 | 101 | removeConstraints(userAddedConstraints) 102 | 103 | */ 104 | var userAddedConstraints: [NSLayoutConstraint] { 105 | return constraints.filter { c in 106 | c is SteviaLayoutConstraint 107 | } 108 | } 109 | } 110 | 111 | // MARK: - Other 112 | 113 | public extension UIView { 114 | 115 | /** 116 | Makes a view follow another view's frame. 117 | For instance if we want a button to be on top of an image : 118 | You can also set how much you want to cover the image, like set the leading, trailing, top and bottom alignment. 119 | 120 | ``` 121 | button.followEdges(image) 122 | ``` 123 | */ 124 | func followEdges(_ otherView: UIView, top : Double = 0.0, bottom : Double = 0.0, leading : Double = 0.0, trailing : Double = 0.0) { 125 | if let spv = superview { 126 | let cs = [ 127 | constraint(item: self, attribute: .top, toItem: otherView, constant: top), 128 | constraint(item: self, attribute: .trailing, toItem: otherView, constant: trailing), 129 | constraint(item: self, attribute: .bottom, toItem: otherView, constant: bottom), 130 | constraint(item: self, attribute: .leading, toItem: otherView, constant: leading) 131 | ] 132 | spv.addConstraints(cs) 133 | } 134 | } 135 | 136 | /** 137 | Enforce a view to keep height and width equal at all times, essentially 138 | forcing it to be a square. 139 | 140 | ``` 141 | image.heightEqualsWidth() 142 | ``` 143 | 144 | - Returns: Itself, enabling chaining, 145 | 146 | */ 147 | @discardableResult 148 | func heightEqualsWidth() -> Self { 149 | if let spv = superview { 150 | let c = constraint(item: self, attribute: .height, toItem: self, attribute: .width) 151 | spv.addConstraint(c) 152 | } 153 | return self 154 | } 155 | 156 | } 157 | #endif 158 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Content.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Content.swift 3 | // LoginStevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 01/10/15. 6 | // Copyright © 2015 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public extension UIButton { 13 | 14 | /** 15 | Sets the title of the button for normal State 16 | 17 | Essentially a shortcut for `setTitle("MyText", forState: .Normal)` 18 | 19 | - Returns: Itself for chaining purposes 20 | */ 21 | @discardableResult 22 | func text(_ t: String) -> Self { 23 | setTitle(t, for: .normal) 24 | return self 25 | } 26 | 27 | /** 28 | Sets the localized key for the button's title in normal State 29 | 30 | Essentially a shortcut for `setTitle(NSLocalizedString("MyText", comment: "") 31 | , forState: .Normal)` 32 | 33 | - Returns: Itself for chaining purposes 34 | */ 35 | @discardableResult 36 | func textKey(_ t: String) -> Self { 37 | text(NSLocalizedString(t, comment: "")) 38 | return self 39 | } 40 | 41 | /** 42 | Sets the image of the button in normal State 43 | 44 | Essentially a shortcut for `setImage(UIImage(named:"X"), forState: .Normal)` 45 | 46 | - Returns: Itself for chaining purposes 47 | */ 48 | @discardableResult 49 | func image(_ s: String) -> Self { 50 | setImage(UIImage(named: s), for: .normal) 51 | return self 52 | } 53 | } 54 | 55 | public extension UITextField { 56 | /** 57 | Sets the textfield placeholder but in a chainable fashion 58 | - Returns: Itself for chaining purposes 59 | */ 60 | @discardableResult 61 | func placeholder(_ t: String) -> Self { 62 | placeholder = t 63 | return self 64 | } 65 | } 66 | 67 | public extension UILabel { 68 | /** 69 | Sets the label text but in a chainable fashion 70 | - Returns: Itself for chaining purposes 71 | */ 72 | @discardableResult 73 | func text(_ t: String) -> Self { 74 | text = t 75 | return self 76 | } 77 | 78 | /** 79 | Sets the label localization key but in a chainable fashion 80 | Essentially a shortcut for `text = NSLocalizedString("X", comment: "")` 81 | - Returns: Itself for chaining purposes 82 | */ 83 | @discardableResult 84 | func textKey(_ t: String) -> Self { 85 | text(NSLocalizedString(t, comment: "")) 86 | return self 87 | } 88 | } 89 | 90 | extension UIImageView { 91 | /** 92 | Sets the image of the imageView but in a chainable fashion 93 | 94 | Essentially a shortcut for `image = UIImage(named: "X")` 95 | 96 | - Returns: Itself for chaining purposes 97 | */ 98 | @discardableResult 99 | public func image(_ t: String) -> Self { 100 | image = UIImage(named: t) 101 | return self 102 | } 103 | } 104 | #endif 105 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+DoubleDash.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+DoubleDash.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 03/05/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | infix operator -- :AdditionPrecedence 13 | 14 | 15 | @discardableResult 16 | public func -- (left: UIView, right: Double) -> PartialConstraint { 17 | return left-right 18 | } 19 | 20 | public func -- (left: UIView, right: CGFloat) -> PartialConstraint { 21 | return left--Double(right) 22 | } 23 | 24 | public func -- (left: UIView, right: Int) -> PartialConstraint { 25 | return left--Double(right) 26 | } 27 | 28 | @discardableResult 29 | @MainActor public func -- (left: SideConstraint, right: UIView) -> UIView { 30 | return left-right 31 | } 32 | 33 | @discardableResult 34 | @MainActor public func -- (left: [UIView], right: SideConstraint) -> [UIView] { 35 | return left-right 36 | } 37 | 38 | @discardableResult 39 | @MainActor public func -- (left: UIView, right: SideConstraint) -> UIView { 40 | return left-right 41 | } 42 | 43 | @discardableResult 44 | @MainActor public func -- (left: PartialConstraint, right: UIView) -> [UIView] { 45 | return left-right 46 | } 47 | 48 | @discardableResult 49 | @MainActor public func -- (left: UIView, right: UIView) -> [UIView] { 50 | return left-right 51 | } 52 | 53 | @discardableResult 54 | public func -- (left: [UIView], right: Double) -> PartialConstraint { 55 | return left-right 56 | } 57 | 58 | @discardableResult 59 | public func -- (left: [UIView], right: CGFloat) -> PartialConstraint { 60 | return left--Double(right) 61 | } 62 | 63 | @discardableResult 64 | public func -- (left: [UIView], right: Int) -> PartialConstraint { 65 | return left--Double(right) 66 | } 67 | 68 | @discardableResult 69 | @MainActor public func -- (left: [UIView], right: UIView) -> [UIView] { 70 | return left-right 71 | } 72 | 73 | @discardableResult 74 | public func -- (left: UIView, right: String) -> Space { 75 | return left-right 76 | } 77 | 78 | @discardableResult 79 | public func -- (left: [UIView], right: String) -> Space { 80 | return left-right 81 | } 82 | 83 | @discardableResult 84 | public func -- (left: Space, right: UIView) -> [UIView] { 85 | return left-right 86 | } 87 | 88 | @discardableResult 89 | public func -- (left: UIView, 90 | right: SteviaFlexibleMargin) -> PartialFlexibleConstraint { 91 | return left-right 92 | } 93 | 94 | @discardableResult 95 | public func -- (left: [UIView], 96 | right: SteviaFlexibleMargin) -> PartialFlexibleConstraint { 97 | return left-right 98 | } 99 | 100 | @discardableResult 101 | @MainActor public func -- (left: PartialFlexibleConstraint, right: UIView) -> [UIView] { 102 | return left-right 103 | } 104 | 105 | @discardableResult 106 | @MainActor public func -- (left: SteviaLeftFlexibleMargin, right: UIView) -> UIView { 107 | return left-right 108 | } 109 | 110 | @discardableResult 111 | @MainActor public func -- (left: UIView, right: SteviaRightFlexibleMargin) -> UIView { 112 | return left-right 113 | } 114 | 115 | @discardableResult 116 | @MainActor public func -- (left: [UIView], right: SteviaRightFlexibleMargin) -> [UIView] { 117 | return left-right 118 | } 119 | #endif 120 | 121 | /// Hyphen Bullet operator is introduced to remove the ambiguity with the language "-" (minus) sign operator. 122 | /// this ambiguity can make tu build times exponentially longer in big layout blocks. 123 | 124 | 125 | #if canImport(UIKit) 126 | import UIKit 127 | 128 | infix operator ⁃ :AdditionPrecedence 129 | 130 | @discardableResult 131 | public func ⁃ (left: UIView, right: Double) -> PartialConstraint { 132 | return left-right 133 | } 134 | 135 | public func ⁃ (left: UIView, right: CGFloat) -> PartialConstraint { 136 | return left⁃Double(right) 137 | } 138 | 139 | public func ⁃ (left: UIView, right: Int) -> PartialConstraint { 140 | return left⁃Double(right) 141 | } 142 | 143 | @discardableResult 144 | @MainActor public func ⁃ (left: SideConstraint, right: UIView) -> UIView { 145 | return left-right 146 | } 147 | 148 | @discardableResult 149 | @MainActor public func ⁃ (left: [UIView], right: SideConstraint) -> [UIView] { 150 | return left-right 151 | } 152 | 153 | @discardableResult 154 | @MainActor public func ⁃ (left: UIView, right: SideConstraint) -> UIView { 155 | return left-right 156 | } 157 | 158 | @discardableResult 159 | @MainActor public func ⁃ (left: PartialConstraint, right: UIView) -> [UIView] { 160 | return left-right 161 | } 162 | 163 | @discardableResult 164 | @MainActor public func ⁃ (left: UIView, right: UIView) -> [UIView] { 165 | return left-right 166 | } 167 | 168 | @discardableResult 169 | public func ⁃ (left: [UIView], right: Double) -> PartialConstraint { 170 | return left-right 171 | } 172 | 173 | @discardableResult 174 | public func ⁃ (left: [UIView], right: CGFloat) -> PartialConstraint { 175 | return left⁃Double(right) 176 | } 177 | 178 | @discardableResult 179 | public func ⁃ (left: [UIView], right: Int) -> PartialConstraint { 180 | return left⁃Double(right) 181 | } 182 | 183 | @discardableResult 184 | @MainActor public func ⁃ (left: [UIView], right: UIView) -> [UIView] { 185 | return left-right 186 | } 187 | 188 | @discardableResult 189 | public func ⁃ (left: UIView, right: String) -> Space { 190 | return left-right 191 | } 192 | 193 | @discardableResult 194 | public func ⁃ (left: [UIView], right: String) -> Space { 195 | return left-right 196 | } 197 | 198 | @discardableResult 199 | public func ⁃ (left: Space, right: UIView) -> [UIView] { 200 | return left-right 201 | } 202 | 203 | @discardableResult 204 | public func ⁃ (left: UIView, 205 | right: SteviaFlexibleMargin) -> PartialFlexibleConstraint { 206 | return left-right 207 | } 208 | 209 | @discardableResult 210 | public func ⁃ (left: [UIView], 211 | right: SteviaFlexibleMargin) -> PartialFlexibleConstraint { 212 | return left-right 213 | } 214 | 215 | @discardableResult 216 | @MainActor public func ⁃ (left: PartialFlexibleConstraint, right: UIView) -> [UIView] { 217 | return left-right 218 | } 219 | 220 | @discardableResult 221 | @MainActor public func ⁃ (left: SteviaLeftFlexibleMargin, right: UIView) -> UIView { 222 | return left-right 223 | } 224 | 225 | @discardableResult 226 | @MainActor public func ⁃ (left: UIView, right: SteviaRightFlexibleMargin) -> UIView { 227 | return left-right 228 | } 229 | 230 | @discardableResult 231 | @MainActor public func ⁃ (left: [UIView], right: SteviaRightFlexibleMargin) -> [UIView] { 232 | return left-right 233 | } 234 | #endif 235 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Equation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Equation.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 21/01/2017. 6 | // Copyright © 2017 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public struct SteviaAttribute { 13 | let view: UIView 14 | let attribute: NSLayoutConstraint.Attribute 15 | let constant: Double? 16 | let multiplier: Double? 17 | 18 | init(view: UIView, attribute: NSLayoutConstraint.Attribute) { 19 | self.view = view 20 | self.attribute = attribute 21 | self.constant = nil 22 | self.multiplier = nil 23 | } 24 | 25 | init(view: UIView, attribute: NSLayoutConstraint.Attribute, constant: Double?, multiplier: Double?) { 26 | self.view = view 27 | self.attribute = attribute 28 | self.constant = constant 29 | self.multiplier = multiplier 30 | } 31 | } 32 | 33 | public extension UIView { 34 | 35 | var Width: SteviaAttribute { 36 | return SteviaAttribute(view: self, attribute: .width) 37 | } 38 | 39 | var Height: SteviaAttribute { 40 | return SteviaAttribute(view: self, attribute: .height) 41 | } 42 | 43 | var Top: SteviaAttribute { 44 | return SteviaAttribute(view: self, attribute: .top) 45 | } 46 | 47 | var Bottom: SteviaAttribute { 48 | return SteviaAttribute(view: self, attribute: .bottom) 49 | } 50 | 51 | var Left: SteviaAttribute { 52 | return SteviaAttribute(view: self, attribute: .left) 53 | } 54 | 55 | var Right: SteviaAttribute { 56 | return SteviaAttribute(view: self, attribute: .right) 57 | } 58 | 59 | var Leading: SteviaAttribute { 60 | return SteviaAttribute(view: self, attribute: .leading) 61 | } 62 | 63 | var Trailing: SteviaAttribute { 64 | return SteviaAttribute(view: self, attribute: .trailing) 65 | } 66 | 67 | var CenterX: SteviaAttribute { 68 | return SteviaAttribute(view: self, attribute: .centerX) 69 | } 70 | 71 | var CenterY: SteviaAttribute { 72 | return SteviaAttribute(view: self, attribute: .centerY) 73 | } 74 | 75 | var FirstBaseline: SteviaAttribute { 76 | return SteviaAttribute(view: self, attribute: .firstBaseline) 77 | } 78 | 79 | var LastBaseline: SteviaAttribute { 80 | return SteviaAttribute(view: self, attribute: .lastBaseline) 81 | } 82 | } 83 | 84 | // MARK: - Equations of type v.P == v'.P' + X 85 | 86 | @discardableResult 87 | @MainActor public func == (left: SteviaAttribute, right: SteviaAttribute) -> NSLayoutConstraint { 88 | let constant = right.constant ?? left.constant ?? 0 89 | let multiplier = right.multiplier ?? left.multiplier ?? 1 90 | 91 | if left.view.superview == right.view.superview { // A and B are at the same level 92 | // Old code 93 | if let spv = left.view.superview { 94 | return spv.addConstraint(item: left.view, 95 | attribute: left.attribute, 96 | toItem: right.view, 97 | attribute: right.attribute, 98 | multiplier: multiplier, 99 | constant: constant) 100 | } 101 | } else if left.view.superview == right.view { // A is in B (first level) 102 | return right.view.addConstraint(item: left.view, 103 | attribute: left.attribute, 104 | toItem: right.view, 105 | attribute: right.attribute, 106 | multiplier: multiplier, 107 | constant: constant) 108 | } else if right.view.superview == left.view { // B is in A (first level) 109 | return left.view.addConstraint(item: right.view, 110 | attribute: right.attribute, 111 | toItem: left.view, 112 | attribute: left.attribute, 113 | multiplier: multiplier, 114 | constant: constant) 115 | } else if left.view.isDescendant(of: right.view) { // A is in B (LOW level) 116 | return right.view.addConstraint(item: left.view, 117 | attribute: left.attribute, 118 | toItem: right.view, 119 | attribute: right.attribute, 120 | multiplier: multiplier, 121 | constant: constant) 122 | } else if right.view.isDescendant(of: left.view) { // B is in A (LOW level) 123 | return left.view.addConstraint(item: left.view, 124 | attribute: left.attribute, 125 | toItem: right.view, 126 | attribute: right.attribute, 127 | multiplier: multiplier, 128 | constant: constant) 129 | } else if let commonParent = commonParent(with: left.view, and: right.view) { // Look for common ancestor 130 | return commonParent.addConstraint(item: left.view, 131 | attribute: left.attribute, 132 | toItem: right.view, 133 | attribute: right.attribute, 134 | multiplier: multiplier, 135 | constant: constant) 136 | } 137 | 138 | return NSLayoutConstraint() 139 | } 140 | 141 | @MainActor func commonParent(with viewA: UIView, and viewB: UIView) -> UIView? { 142 | 143 | // Both views should have a superview 144 | guard viewA.superview != nil && viewB.superview != nil else { 145 | return nil 146 | } 147 | 148 | // Find the common parent 149 | var spv = viewA.superview 150 | while spv != nil { 151 | if viewA.isDescendant(of: spv!) && viewB.isDescendant(of: spv!) { 152 | return spv 153 | } else { 154 | spv = spv?.superview 155 | } 156 | } 157 | return nil 158 | } 159 | 160 | @discardableResult 161 | @MainActor public func >= (left: SteviaAttribute, right: SteviaAttribute) -> NSLayoutConstraint { 162 | return applyRelation(left: left, right: right, relateBy: .greaterThanOrEqual) 163 | } 164 | 165 | @discardableResult 166 | @MainActor public func <= (left: SteviaAttribute, right: SteviaAttribute) -> NSLayoutConstraint { 167 | return applyRelation(left: left, right: right, relateBy: .lessThanOrEqual) 168 | } 169 | 170 | @MainActor private func applyRelation(left: SteviaAttribute, right: SteviaAttribute, relateBy: NSLayoutConstraint.Relation) -> NSLayoutConstraint { 171 | let constant = right.constant ?? 0 172 | let multiplier = right.multiplier ?? 1 173 | if let spv = left.view.superview { 174 | return spv.addConstraint(item: left.view, 175 | attribute: left.attribute, 176 | relatedBy: relateBy, 177 | toItem: right.view, 178 | attribute: right.attribute, 179 | multiplier: multiplier, 180 | constant: constant) 181 | } 182 | return NSLayoutConstraint() 183 | } 184 | 185 | @discardableResult 186 | public func + (left: SteviaAttribute, right: Double) -> SteviaAttribute { 187 | SteviaAttribute(view: left.view, attribute: left.attribute, constant: right, multiplier: left.multiplier) 188 | } 189 | 190 | @discardableResult 191 | public func + (left: SteviaAttribute, right: CGFloat) -> SteviaAttribute { 192 | left + Double(right) 193 | } 194 | 195 | @discardableResult 196 | public func + (left: SteviaAttribute, right: Int) -> SteviaAttribute { 197 | left + Double(right) 198 | } 199 | 200 | @discardableResult 201 | public func - (left: SteviaAttribute, right: Double) -> SteviaAttribute { 202 | SteviaAttribute(view: left.view, attribute: left.attribute, constant: -right, multiplier: left.multiplier) 203 | } 204 | 205 | @discardableResult 206 | public func - (left: SteviaAttribute, right: CGFloat) -> SteviaAttribute { 207 | left - Double(right) 208 | } 209 | 210 | @discardableResult 211 | public func - (left: SteviaAttribute, right: Int) -> SteviaAttribute { 212 | left - Double(right) 213 | } 214 | 215 | @discardableResult 216 | public func * (left: SteviaAttribute, right: Double) -> SteviaAttribute { 217 | SteviaAttribute(view: left.view, attribute: left.attribute, constant: left.constant, multiplier: right) 218 | } 219 | 220 | @discardableResult 221 | public func * (left: SteviaAttribute, right: CGFloat) -> SteviaAttribute { 222 | left * Double(right) 223 | } 224 | 225 | @discardableResult 226 | public func * (left: SteviaAttribute, right: Int) -> SteviaAttribute { 227 | left * Double(right) 228 | } 229 | 230 | @discardableResult 231 | public func / (left: SteviaAttribute, right: Double) -> SteviaAttribute { 232 | left * (1/right) 233 | } 234 | 235 | @discardableResult 236 | public func / (left: SteviaAttribute, right: CGFloat) -> SteviaAttribute { 237 | left / Double(right) 238 | } 239 | 240 | @discardableResult 241 | public func / (left: SteviaAttribute, right: Int) -> SteviaAttribute { 242 | left / Double(right) 243 | } 244 | 245 | 246 | @discardableResult 247 | public func % (left: Double, right: SteviaAttribute) -> SteviaAttribute { 248 | right * (left/100) 249 | } 250 | 251 | @discardableResult 252 | public func % (left: CGFloat, right: SteviaAttribute) -> SteviaAttribute { 253 | Double(left) % right 254 | } 255 | 256 | @discardableResult 257 | public func % (left: Int, right: SteviaAttribute) -> SteviaAttribute { 258 | Double(left) % right 259 | } 260 | 261 | 262 | // MARK: - Equations of type v.P == X 263 | 264 | @discardableResult 265 | @MainActor public func == (left: SteviaAttribute, right: Double) -> NSLayoutConstraint { 266 | if let spv = left.view.superview { 267 | var toItem: UIView? = spv 268 | var constant = right 269 | if left.attribute == .width || left.attribute == .height { 270 | toItem = nil 271 | } 272 | if left.attribute == .bottom || left.attribute == .right { 273 | constant = -constant 274 | } 275 | return spv.addConstraint(item: left.view, 276 | attribute: left.attribute, 277 | toItem: toItem, 278 | constant: Double(constant)) 279 | } 280 | return NSLayoutConstraint() 281 | } 282 | 283 | @discardableResult 284 | @MainActor public func == (left: SteviaAttribute, right: CGFloat) -> NSLayoutConstraint { 285 | left == Double(right) 286 | } 287 | 288 | @discardableResult 289 | @MainActor public func == (left: SteviaAttribute, right: Int) -> NSLayoutConstraint { 290 | left == Double(right) 291 | } 292 | 293 | @discardableResult 294 | @MainActor public func >= (left: SteviaAttribute, right: Double) -> NSLayoutConstraint { 295 | if let spv = left.view.superview { 296 | var toItem: UIView? = spv 297 | var constant = right 298 | if left.attribute == .width || left.attribute == .height { 299 | toItem = nil 300 | } 301 | if left.attribute == .bottom || left.attribute == .right { 302 | constant = -constant 303 | } 304 | return spv.addConstraint(item: left.view, 305 | attribute: left.attribute, 306 | relatedBy: .greaterThanOrEqual, 307 | toItem: toItem, 308 | constant: Double(constant)) 309 | } 310 | return NSLayoutConstraint() 311 | } 312 | 313 | @discardableResult 314 | @MainActor public func >= (left: SteviaAttribute, right: CGFloat) -> NSLayoutConstraint { 315 | left >= Double(right) 316 | } 317 | 318 | @discardableResult 319 | @MainActor public func >= (left: SteviaAttribute, right: Int) -> NSLayoutConstraint { 320 | left >= Double(right) 321 | } 322 | 323 | @discardableResult 324 | @MainActor public func <= (left: SteviaAttribute, right: Double) -> NSLayoutConstraint { 325 | if let spv = left.view.superview { 326 | var toItem: UIView? = spv 327 | var constant = right 328 | if left.attribute == .width || left.attribute == .height { 329 | toItem = nil 330 | } 331 | if left.attribute == .bottom || left.attribute == .right { 332 | constant = -constant 333 | } 334 | return spv.addConstraint(item: left.view, 335 | attribute: left.attribute, 336 | relatedBy: .lessThanOrEqual, 337 | toItem: toItem, 338 | constant: Double(constant)) 339 | } 340 | return NSLayoutConstraint() 341 | } 342 | 343 | @discardableResult 344 | @MainActor public func <= (left: SteviaAttribute, right: CGFloat) -> NSLayoutConstraint { 345 | left <= Double(right) 346 | } 347 | 348 | @discardableResult 349 | @MainActor public func <= (left: SteviaAttribute, right: Int) -> NSLayoutConstraint { 350 | left <= Double(right) 351 | } 352 | #endif 353 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Fill.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Fill.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 10/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public extension UIView { 13 | 14 | /** 15 | Adds the constraints needed for the view to fill its `superview`. 16 | A padding can be used to apply equal spaces between the view and its superview 17 | */ 18 | @discardableResult 19 | func fillContainer(padding: Double = 0) -> Self { 20 | fillHorizontally(padding: padding) 21 | fillVertically(padding: padding) 22 | return self 23 | } 24 | 25 | /** 26 | Adds the constraints needed for the view to fill its `superview`. 27 | A padding can be used to apply equal spaces between the view and its superview 28 | */ 29 | @discardableResult 30 | func fillContainer(padding: CGFloat) -> Self { 31 | fillContainer(padding: Double(padding)) 32 | } 33 | 34 | /** 35 | Adds the constraints needed for the view to fill its `superview`. 36 | A padding can be used to apply equal spaces between the view and its superview 37 | */ 38 | @discardableResult 39 | func fillContainer(padding: Int) -> Self { 40 | fillContainer(padding: Double(padding)) 41 | } 42 | 43 | /** 44 | Adds the constraints needed for the view to fill its `superview` Vertically. 45 | A padding can be used to apply equal spaces between the view and its superview 46 | */ 47 | @discardableResult 48 | func fillVertically(padding: Double = 0) -> Self { 49 | fill(.vertical, points: padding) 50 | } 51 | 52 | /** 53 | Adds the constraints needed for the view to fill its `superview` Vertically. 54 | A padding can be used to apply equal spaces between the view and its superview 55 | */ 56 | @discardableResult 57 | func fillVertically(padding: CGFloat) -> Self { 58 | fillVertically(padding: Double(padding)) 59 | } 60 | 61 | /** 62 | Adds the constraints needed for the view to fill its `superview` Vertically. 63 | A padding can be used to apply equal spaces between the view and its superview 64 | */ 65 | @discardableResult 66 | func fillVertically(padding: Int) -> Self { 67 | fillVertically(padding: Double(padding)) 68 | } 69 | 70 | /** 71 | Adds the constraints needed for the view to fill its `superview` Horizontally. 72 | A padding can be used to apply equal spaces between the view and its superview 73 | */ 74 | @discardableResult 75 | func fillHorizontally(padding: Double = 0) -> Self { 76 | fill(.horizontal, points: padding) 77 | } 78 | 79 | /** 80 | Adds the constraints needed for the view to fill its `superview` Horizontally. 81 | A padding can be used to apply equal spaces between the view and its superview 82 | */ 83 | @discardableResult 84 | func fillHorizontally(padding: CGFloat) -> Self { 85 | fillHorizontally(padding: Double(padding)) 86 | } 87 | 88 | /** 89 | Adds the constraints needed for the view to fill its `superview` Horizontally. 90 | A padding can be used to apply equal spaces between the view and its superview 91 | */ 92 | @discardableResult 93 | func fillHorizontally(padding: Int) -> Self { 94 | fillHorizontally(padding: Double(padding)) 95 | } 96 | 97 | fileprivate func fill(_ axis: NSLayoutConstraint.Axis, points: Double = 0) -> Self { 98 | let a: NSLayoutConstraint.Attribute = axis == .vertical ? .top : .leading 99 | let b: NSLayoutConstraint.Attribute = axis == .vertical ? .bottom : .trailing 100 | if let spv = superview { 101 | let c1 = constraint(item: self, attribute: a, toItem: spv, constant: points) 102 | let c2 = constraint(item: self, attribute: b, toItem: spv, constant: -points) 103 | spv.addConstraints([c1, c2]) 104 | } 105 | return self 106 | } 107 | } 108 | #endif 109 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+FlexibleMargin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+FlexibleMargin.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 10/07/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | prefix operator >= 13 | @discardableResult 14 | public prefix func >= (p: Double) -> SteviaFlexibleMargin { 15 | SteviaFlexibleMargin(points: p, relation: .greaterThanOrEqual) 16 | } 17 | 18 | @discardableResult 19 | public prefix func >= (p: CGFloat) -> SteviaFlexibleMargin { 20 | >=Double(p) 21 | } 22 | 23 | @discardableResult 24 | public prefix func >= (p: Int) -> SteviaFlexibleMargin { 25 | >=Double(p) 26 | } 27 | 28 | prefix operator <= 29 | @discardableResult 30 | public prefix func <= (p: Double) -> SteviaFlexibleMargin { 31 | SteviaFlexibleMargin(points: p, relation: .lessThanOrEqual) 32 | } 33 | 34 | @discardableResult 35 | public prefix func <= (p: CGFloat) -> SteviaFlexibleMargin { 36 | <=Double(p) 37 | } 38 | 39 | @discardableResult 40 | public prefix func <= (p: Int) -> SteviaFlexibleMargin { 41 | <=Double(p) 42 | } 43 | 44 | public struct SteviaFlexibleMargin { 45 | var points: Double! 46 | var relation: NSLayoutConstraint.Relation! 47 | } 48 | 49 | public struct PartialFlexibleConstraint { 50 | var fm: SteviaFlexibleMargin! 51 | var view1: UIView? 52 | var views: [UIView]? 53 | } 54 | 55 | @discardableResult 56 | public func - (left: UIView, 57 | right: SteviaFlexibleMargin) -> PartialFlexibleConstraint { 58 | return PartialFlexibleConstraint(fm: right, view1: left, views: nil) 59 | } 60 | 61 | @discardableResult 62 | public func - (left: [UIView], 63 | right: SteviaFlexibleMargin) -> PartialFlexibleConstraint { 64 | return PartialFlexibleConstraint(fm: right, view1: nil, views: left) 65 | } 66 | 67 | @discardableResult 68 | @MainActor public func - (left: PartialFlexibleConstraint, right: UIView) -> [UIView] { 69 | if let views = left.views { 70 | if let spv = right.superview { 71 | let c = constraint(item: right, attribute: .leading, 72 | relatedBy: left.fm.relation, toItem: views.last, 73 | attribute: .trailing, 74 | constant: left.fm.points) 75 | spv.addConstraint(c) 76 | } 77 | return views + [right] 78 | } else { 79 | if let spv = right.superview { 80 | let c = constraint(item: right, attribute: .leading, 81 | relatedBy: left.fm.relation, toItem: left.view1!, 82 | attribute: .trailing, 83 | constant: left.fm.points) 84 | spv.addConstraint(c) 85 | } 86 | return [left.view1!, right] 87 | } 88 | } 89 | 90 | // Left Flexible margin 91 | 92 | public struct SteviaLeftFlexibleMargin { 93 | let fm: SteviaFlexibleMargin 94 | } 95 | 96 | @discardableResult 97 | public prefix func |- (fm: SteviaFlexibleMargin) -> SteviaLeftFlexibleMargin { 98 | return SteviaLeftFlexibleMargin(fm: fm) 99 | } 100 | 101 | @discardableResult 102 | @MainActor public func - (left: SteviaLeftFlexibleMargin, right: UIView) -> UIView { 103 | if let spv = right.superview { 104 | let c = constraint(item: right, attribute: .leading, 105 | relatedBy: left.fm.relation, toItem: spv, 106 | attribute: .leading, 107 | constant: left.fm.points) 108 | spv.addConstraint(c) 109 | } 110 | return right 111 | } 112 | 113 | // Right Flexible margin 114 | 115 | public struct SteviaRightFlexibleMargin { 116 | let fm: SteviaFlexibleMargin 117 | } 118 | 119 | @discardableResult 120 | public postfix func -| (fm: SteviaFlexibleMargin) -> SteviaRightFlexibleMargin { 121 | return SteviaRightFlexibleMargin(fm: fm) 122 | } 123 | 124 | @discardableResult 125 | @MainActor public func - (left: UIView, right: SteviaRightFlexibleMargin) -> UIView { 126 | if let spv = left.superview { 127 | let c = constraint(item: spv, attribute: .trailing, 128 | relatedBy: right.fm.relation, toItem: left, 129 | attribute: .trailing, 130 | constant: right.fm.points) 131 | spv.addConstraint(c) 132 | } 133 | return left 134 | } 135 | 136 | @discardableResult 137 | @MainActor public func - (left: [UIView], right: SteviaRightFlexibleMargin) -> [UIView] { 138 | if let spv = left.last!.superview { 139 | let c = constraint(item: spv, attribute: .trailing, 140 | relatedBy: right.fm.relation, 141 | toItem: left.last!, 142 | attribute: .trailing, 143 | constant: right.fm.points) 144 | spv.addConstraint(c) 145 | } 146 | return left 147 | } 148 | #endif 149 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+GetConstraint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+GetConstraint.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 12/03/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public extension UIView { 13 | 14 | /** Gets the left constraint if found. 15 | 16 | Example Usage for changing left margin of a label : 17 | ``` 18 | label.leftConstraint?.constant = 10 19 | 20 | // Animate if needed 21 | UIView.animateWithDuration(0.3, animations:layoutIfNeeded) 22 | ``` 23 | - Returns: The left NSLayoutConstraint if found. 24 | */ 25 | var leftConstraint: NSLayoutConstraint? { 26 | return constraintForView(self, attribute: .left) 27 | } 28 | 29 | /** Gets the right constraint if found. 30 | 31 | Example Usage for changing right margin of a label : 32 | 33 | ``` 34 | label.rightConstraint?.constant = 10 35 | 36 | // Animate if needed 37 | UIView.animateWithDuration(0.3, animations:layoutIfNeeded) 38 | ``` 39 | - Returns: The right NSLayoutConstraint if found. 40 | */ 41 | var rightConstraint: NSLayoutConstraint? { 42 | return constraintForView(self, attribute: .right) 43 | } 44 | 45 | /** Gets the top constraint if found. 46 | 47 | Example Usage for changing top margin of a label : 48 | 49 | ``` 50 | label.topConstraint?.constant = 10 51 | 52 | // Animate if needed 53 | UIView.animateWithDuration(0.3, animations:layoutIfNeeded) 54 | ``` 55 | - Returns: The top NSLayoutConstraint if found. 56 | */ 57 | var topConstraint: NSLayoutConstraint? { 58 | return constraintForView(self, attribute: .top) 59 | } 60 | 61 | /** Gets the bottom constraint if found. 62 | 63 | Example Usage for changing bottom margin of a label : 64 | 65 | ``` 66 | label.bottomConstraint?.constant = 10 67 | 68 | // Animate if needed 69 | UIView.animateWithDuration(0.3, animations:layoutIfNeeded) 70 | ``` 71 | - Returns: The bottom NSLayoutConstraint if found. 72 | */ 73 | var bottomConstraint: NSLayoutConstraint? { 74 | return constraintForView(self, attribute: .bottom) 75 | } 76 | 77 | /** Gets the height constraint if found. 78 | 79 | Example Usage for changing height property of a label : 80 | 81 | ``` 82 | label.heightConstraint?.constant = 10 83 | 84 | // Animate if needed 85 | UIView.animateWithDuration(0.3, animations:layoutIfNeeded) 86 | ``` 87 | - Returns: The height NSLayoutConstraint if found. 88 | */ 89 | var heightConstraint: NSLayoutConstraint? { 90 | return constraintForView(self, attribute: .height) 91 | } 92 | 93 | /** Gets the width constraint if found. 94 | 95 | Example Usage for changing width property of a label : 96 | 97 | ``` 98 | label.widthConstraint?.constant = 10 99 | 100 | // Animate if needed 101 | UIView.animateWithDuration(0.3, animations:layoutIfNeeded) 102 | ``` 103 | - Returns: The width NSLayoutConstraint if found. 104 | */ 105 | var widthConstraint: NSLayoutConstraint? { 106 | return constraintForView(self, attribute: .width) 107 | } 108 | 109 | /** Gets the trailing constraint if found. 110 | 111 | Example Usage for changing width property of a label : 112 | 113 | ``` 114 | label.trailingConstraint?.constant = 10 115 | 116 | // Animate if needed 117 | UIView.animateWithDuration(0.3, animations:layoutIfNeeded) 118 | ``` 119 | - Returns: The trailing NSLayoutConstraint if found. 120 | */ 121 | var trailingConstraint: NSLayoutConstraint? { 122 | return constraintForView(self, attribute: .trailing) 123 | } 124 | 125 | /** Gets the leading constraint if found. 126 | 127 | Example Usage for changing width property of a label : 128 | 129 | ``` 130 | label.leadingConstraint?.constant = 10 131 | 132 | // Animate if needed 133 | UIView.animateWithDuration(0.3, animations:layoutIfNeeded) 134 | ``` 135 | - Returns: The leading NSLayoutConstraint if found. 136 | */ 137 | var leadingConstraint: NSLayoutConstraint? { 138 | return constraintForView(self, attribute: .leading) 139 | } 140 | 141 | /** Gets the centerX constraint if found. 142 | 143 | Example Usage for changing width property of a label : 144 | 145 | ``` 146 | label.centerXConstraint?.constant = 10 147 | 148 | // Animate if needed 149 | UIView.animateWithDuration(0.3, animations:layoutIfNeeded) 150 | ``` 151 | - Returns: The width NSLayoutConstraint if found. 152 | */ 153 | var centerXConstraint: NSLayoutConstraint? { 154 | return constraintForView(self, attribute: .centerX) 155 | } 156 | 157 | /** Gets the centerY constraint if found. 158 | 159 | Example Usage for changing width property of a label : 160 | 161 | ``` 162 | label.centerYConstraint?.constant = 10 163 | 164 | // Animate if needed 165 | UIView.animateWithDuration(0.3, animations:layoutIfNeeded) 166 | ``` 167 | - Returns: The width NSLayoutConstraint if found. 168 | */ 169 | var centerYConstraint: NSLayoutConstraint? { 170 | return constraintForView(self, attribute: .centerY) 171 | } 172 | } 173 | 174 | @MainActor func constraintForView(_ v: UIView, attribute: NSLayoutConstraint.Attribute) -> NSLayoutConstraint? { 175 | 176 | func lookForConstraint(in view: UIView?) -> NSLayoutConstraint? { 177 | guard let constraints = view?.constraints else { 178 | return nil 179 | } 180 | for c in constraints { 181 | if let fi = c.firstItem as? NSObject, fi == v && c.firstAttribute == attribute { 182 | return c 183 | } else if let si = c.secondItem as? NSObject, si == v && c.secondAttribute == attribute { 184 | return c 185 | } 186 | } 187 | return nil 188 | } 189 | 190 | // Width and height constraints added via widthAnchor/heightAnchors are 191 | // added on the view itself. 192 | if (attribute == .width || attribute == .height) { 193 | return lookForConstraint(in: v.superview) ?? lookForConstraint(in: v) 194 | } 195 | 196 | // Look for constraint on superview. 197 | return lookForConstraint(in: v.superview) 198 | } 199 | #endif 200 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Hierarchy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Hierarchy.swift 3 | // LoginStevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 01/10/15. 6 | // Copyright © 2015 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | @resultBuilder public struct SubviewsBuilder { 13 | public static func buildBlock(_ content: UIView...) -> [UIView] { 14 | return content 15 | } 16 | } 17 | 18 | public extension UIView { 19 | 20 | @available(*, deprecated, renamed: "subviews") 21 | @discardableResult 22 | func sv(_ subViews: UIView...) -> UIView { 23 | subviews(subViews) 24 | } 25 | 26 | /** 27 | Defines the view hierachy for the view. 28 | 29 | Esentially, this is just a shortcut to `addSubview` 30 | and 'translatesAutoresizingMaskIntoConstraints = false' 31 | 32 | 33 | 34 | ``` 35 | class MyView: UIView { 36 | 37 | let email = UITextField() 38 | let password = UITextField() 39 | let login = UIButton() 40 | 41 | convenience init() { 42 | self.init(frame: CGRect.zero) 43 | 44 | subviews( 45 | email, 46 | password, 47 | login 48 | ) 49 | ... 50 | 51 | } 52 | } 53 | 54 | ``` 55 | 56 | - Returns: Itself to enable nested layouts. 57 | */ 58 | @discardableResult 59 | func subviews(_ subViews: UIView...) -> UIView { 60 | subviews(subViews) 61 | } 62 | 63 | /** 64 | Defines the view hierachy for the view. 65 | 66 | Esentially, this is just a shortcut to `addSubview` 67 | and 'translatesAutoresizingMaskIntoConstraints = false' 68 | 69 | 70 | 71 | ``` 72 | class MyView: UIView { 73 | 74 | let email = UITextField() 75 | let password = UITextField() 76 | let login = UIButton() 77 | 78 | convenience init() { 79 | self.init(frame: CGRect.zero) 80 | 81 | subviews { 82 | email 83 | password 84 | login 85 | } 86 | ... 87 | 88 | } 89 | } 90 | 91 | ``` 92 | 93 | - Returns: Itself to enable nested layouts. 94 | */ 95 | @discardableResult 96 | func subviews(@SubviewsBuilder content: () -> [UIView]) -> UIView { 97 | subviews(content()) 98 | } 99 | 100 | /** 101 | Defines the view hierachy for the view. 102 | 103 | Esentially, this is just a shortcut to `addSubview` 104 | and 'translatesAutoresizingMaskIntoConstraints = false' 105 | 106 | 107 | 108 | ``` 109 | class MyView: UIView { 110 | 111 | let email = UITextField() 112 | let password = UITextField() 113 | let login = UIButton() 114 | 115 | convenience init() { 116 | self.init(frame: CGRect.zero) 117 | 118 | subviews { 119 | email 120 | password 121 | login 122 | } 123 | ... 124 | 125 | } 126 | } 127 | 128 | ``` 129 | 130 | - Returns: Itself to enable nested layouts. 131 | */ 132 | @discardableResult 133 | func subviews(@SubviewsBuilder content: () -> UIView) -> UIView { 134 | let subview = content() 135 | subviews(subview) 136 | return self 137 | } 138 | 139 | /** 140 | Defines the view hierachy for the view. 141 | 142 | Esentially, this is just a shortcut to `addSubview` 143 | and 'translatesAutoresizingMaskIntoConstraints = false' 144 | 145 | 146 | ``` 147 | class MyView: UIView { 148 | 149 | let email = UITextField() 150 | let password = UITextField() 151 | let login = UIButton() 152 | 153 | convenience init() { 154 | self.init(frame: CGRect.zero) 155 | 156 | sv( 157 | email, 158 | password, 159 | login 160 | ) 161 | ... 162 | 163 | } 164 | } 165 | 166 | ``` 167 | 168 | - Returns: Itself to enable nested layouts. 169 | */ 170 | @objc 171 | @available(*, deprecated, renamed: "subviews") 172 | @discardableResult 173 | func sv(_ subViews: [UIView]) -> UIView { 174 | subviews(subViews) 175 | } 176 | 177 | 178 | @discardableResult 179 | @objc 180 | func subviews(_ subViews: [UIView]) -> UIView { 181 | for sv in subViews { 182 | addSubview(sv) 183 | sv.translatesAutoresizingMaskIntoConstraints = false 184 | } 185 | return self 186 | } 187 | } 188 | 189 | public extension UITableViewCell { 190 | 191 | /** 192 | Defines the view hierachy for the view. 193 | 194 | Esentially, this is just a shortcut to `contentView.addSubview` 195 | and 'translatesAutoresizingMaskIntoConstraints = false' 196 | 197 | ``` 198 | class NotificationCell: UITableViewCell { 199 | 200 | var avatar = UIImageView() 201 | var name = UILabel() 202 | var followButton = UIButton() 203 | 204 | required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } 205 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 206 | super.init(style: style, reuseIdentifier: reuseIdentifier) { 207 | 208 | sv( 209 | avatar, 210 | name, 211 | followButton 212 | ) 213 | ... 214 | 215 | } 216 | } 217 | ``` 218 | 219 | - Returns: Itself to enable nested layouts. 220 | */ 221 | @available(*, deprecated, renamed: "subviews") 222 | @discardableResult 223 | override func sv(_ subViews: [UIView]) -> UIView { 224 | contentView.subviews(subViews) 225 | } 226 | 227 | /** 228 | Defines the view hierachy for the view. 229 | 230 | Esentially, this is just a shortcut to `contentView.addSubview` 231 | and 'translatesAutoresizingMaskIntoConstraints = false' 232 | 233 | ``` 234 | class NotificationCell: UITableViewCell { 235 | 236 | var avatar = UIImageView() 237 | var name = UILabel() 238 | var followButton = UIButton() 239 | 240 | required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } 241 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) { 242 | super.init(style: style, reuseIdentifier: reuseIdentifier) { 243 | 244 | subviews( 245 | avatar, 246 | name, 247 | followButton 248 | ) 249 | ... 250 | 251 | } 252 | } 253 | ``` 254 | 255 | - Returns: Itself to enable nested layouts. 256 | */ 257 | @discardableResult 258 | override func subviews(_ subViews: [UIView]) -> UIView { 259 | contentView.subviews(subViews) 260 | } 261 | } 262 | 263 | public extension UICollectionViewCell { 264 | /** 265 | Defines the view hierachy for the view. 266 | 267 | Esentially, this is just a shortcut to `contentView.addSubview` 268 | and 'translatesAutoresizingMaskIntoConstraints = false' 269 | 270 | ``` 271 | class PhotoCollectionViewCell: UICollectionViewCell { 272 | 273 | var avatar = UIImageView() 274 | var name = UILabel() 275 | var followButton = UIButton() 276 | 277 | 278 | required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 279 | override init(frame: CGRect) { 280 | super.init(frame: frame) 281 | 282 | subviews( 283 | avatar, 284 | name, 285 | followButton 286 | ) 287 | ... 288 | 289 | } 290 | } 291 | ``` 292 | 293 | - Returns: Itself to enable nested layouts. 294 | */ 295 | @available(*, deprecated, renamed: "subviews") 296 | @discardableResult 297 | override func sv(_ subViews: [UIView]) -> UIView { 298 | contentView.subviews(subViews) 299 | } 300 | 301 | @discardableResult 302 | override func subviews(_ subViews: [UIView]) -> UIView { 303 | contentView.subviews(subViews) 304 | } 305 | } 306 | 307 | 308 | public extension UIStackView { 309 | 310 | @discardableResult 311 | func arrangedSubviews(@SubviewsBuilder content: () -> [UIView]) -> UIView { 312 | arrangedSubviews(content()) 313 | } 314 | 315 | @discardableResult 316 | func arrangedSubviews(@SubviewsBuilder content: () -> UIView) -> UIView { 317 | arrangedSubviews([content()]) 318 | } 319 | 320 | @discardableResult 321 | private func arrangedSubviews(_ subViews: UIView...) -> UIView { 322 | arrangedSubviews(subViews) 323 | } 324 | 325 | @discardableResult 326 | func arrangedSubviews(_ subViews: [UIView]) -> UIView { 327 | subViews.forEach { addArrangedSubview($0) } 328 | return self 329 | } 330 | } 331 | 332 | #endif 333 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+LayoutAnchors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+LayoutAnchors.swift 3 | // Stevia 4 | // 5 | // Created by Sacha DSO on 09/10/2017. 6 | // Copyright © 2017 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | @available(iOS 9.0, *) 13 | public struct SteviaLayoutYAxisAnchor { 14 | let anchor: NSLayoutYAxisAnchor 15 | let constant: Double 16 | 17 | init(anchor: NSLayoutYAxisAnchor, constant: Double = 0) { 18 | self.anchor = anchor 19 | self.constant = constant 20 | } 21 | } 22 | 23 | @available(iOS 9.0, *) 24 | public struct SteviaLayoutXAxisAnchor { 25 | let anchor: NSLayoutXAxisAnchor 26 | let constant: Double 27 | 28 | init(anchor: NSLayoutXAxisAnchor, constant: Double = 0) { 29 | self.anchor = anchor 30 | self.constant = constant 31 | } 32 | } 33 | 34 | @available(iOS 9.0, *) 35 | public extension UILayoutGuide { 36 | 37 | var Top: SteviaLayoutYAxisAnchor { 38 | return SteviaLayoutYAxisAnchor(anchor: topAnchor) 39 | } 40 | 41 | var Bottom: SteviaLayoutYAxisAnchor { 42 | return SteviaLayoutYAxisAnchor(anchor: bottomAnchor) 43 | } 44 | 45 | var Left: SteviaLayoutXAxisAnchor { 46 | return SteviaLayoutXAxisAnchor(anchor: leftAnchor) 47 | } 48 | 49 | var Right: SteviaLayoutXAxisAnchor { 50 | return SteviaLayoutXAxisAnchor(anchor: rightAnchor) 51 | } 52 | 53 | var Leading: SteviaLayoutXAxisAnchor { 54 | return SteviaLayoutXAxisAnchor(anchor: leadingAnchor) 55 | } 56 | 57 | var Trailing: SteviaLayoutXAxisAnchor { 58 | return SteviaLayoutXAxisAnchor(anchor: trailingAnchor) 59 | } 60 | 61 | var CenterX: SteviaLayoutXAxisAnchor { 62 | return SteviaLayoutXAxisAnchor(anchor: centerXAnchor) 63 | } 64 | 65 | var CenterY: SteviaLayoutYAxisAnchor { 66 | return SteviaLayoutYAxisAnchor(anchor: centerYAnchor) 67 | } 68 | } 69 | 70 | @available(iOS 9.0, *) 71 | @discardableResult 72 | @MainActor public func == (left: SteviaAttribute, right: SteviaLayoutYAxisAnchor) -> NSLayoutConstraint { 73 | 74 | var constraint = NSLayoutConstraint() 75 | 76 | if left.attribute == .top { 77 | constraint = left.view.topAnchor.constraint(equalTo: right.anchor, constant: CGFloat(right.constant)) 78 | } 79 | 80 | if left.attribute == .bottom { 81 | constraint = left.view.bottomAnchor.constraint(equalTo: right.anchor, constant: CGFloat(right.constant)) 82 | } 83 | 84 | if left.attribute == .centerY { 85 | constraint = left.view.centerYAnchor.constraint(equalTo: right.anchor, constant: CGFloat(right.constant)) 86 | } 87 | 88 | constraint.isActive = true 89 | return constraint 90 | } 91 | 92 | @available(iOS 9.0, *) 93 | @discardableResult 94 | @MainActor public func == (left: SteviaAttribute, right: SteviaLayoutXAxisAnchor) -> NSLayoutConstraint { 95 | 96 | var constraint = NSLayoutConstraint() 97 | 98 | if left.attribute == .left { 99 | constraint = left.view.leftAnchor.constraint(equalTo: right.anchor, constant: CGFloat(right.constant)) 100 | } 101 | 102 | if left.attribute == .right { 103 | constraint = left.view.rightAnchor.constraint(equalTo: right.anchor, constant: CGFloat(right.constant)) 104 | } 105 | 106 | if left.attribute == .leading { 107 | constraint = left.view.leadingAnchor.constraint(equalTo: right.anchor, constant: CGFloat(right.constant)) 108 | } 109 | 110 | if left.attribute == .trailing { 111 | constraint = left.view.trailingAnchor.constraint(equalTo: right.anchor, constant: CGFloat(right.constant)) 112 | } 113 | 114 | if left.attribute == .centerX { 115 | constraint = left.view.centerXAnchor.constraint(equalTo: right.anchor, constant: CGFloat(right.constant)) 116 | } 117 | 118 | constraint.isActive = true 119 | return constraint 120 | } 121 | 122 | // SteviaLayoutYAxisAnchor 123 | 124 | @available(iOS 9.0, *) 125 | @discardableResult 126 | public func + (left: SteviaLayoutYAxisAnchor, right: Double) -> SteviaLayoutYAxisAnchor { 127 | return SteviaLayoutYAxisAnchor(anchor: left.anchor, constant: right) 128 | } 129 | 130 | @available(iOS 9.0, *) 131 | @discardableResult 132 | public func - (left: SteviaLayoutYAxisAnchor, right: Double) -> SteviaLayoutYAxisAnchor { 133 | return SteviaLayoutYAxisAnchor(anchor: left.anchor, constant: -right) 134 | } 135 | 136 | @available(iOS 9.0, *) 137 | @discardableResult 138 | public func + (left: SteviaLayoutXAxisAnchor, right: Double) -> SteviaLayoutXAxisAnchor { 139 | return SteviaLayoutXAxisAnchor(anchor: left.anchor, constant: right) 140 | } 141 | 142 | @available(iOS 9.0, *) 143 | @discardableResult 144 | public func - (left: SteviaLayoutXAxisAnchor, right: Double) -> SteviaLayoutXAxisAnchor { 145 | return SteviaLayoutXAxisAnchor(anchor: left.anchor, constant: -right) 146 | } 147 | 148 | @available(iOS 9.0, *) 149 | @discardableResult 150 | public func + (left: SteviaLayoutYAxisAnchor, right: CGFloat) -> SteviaLayoutYAxisAnchor { 151 | return SteviaLayoutYAxisAnchor(anchor: left.anchor, constant: Double(right)) 152 | } 153 | 154 | @available(iOS 9.0, *) 155 | @discardableResult 156 | public func - (left: SteviaLayoutYAxisAnchor, right: CGFloat) -> SteviaLayoutYAxisAnchor { 157 | return SteviaLayoutYAxisAnchor(anchor: left.anchor, constant: Double(-right)) 158 | } 159 | 160 | @available(iOS 9.0, *) 161 | @discardableResult 162 | public func + (left: SteviaLayoutXAxisAnchor, right: CGFloat) -> SteviaLayoutXAxisAnchor { 163 | return SteviaLayoutXAxisAnchor(anchor: left.anchor, constant: Double(right)) 164 | } 165 | 166 | @available(iOS 9.0, *) 167 | @discardableResult 168 | public func - (left: SteviaLayoutXAxisAnchor, right: CGFloat) -> SteviaLayoutXAxisAnchor { 169 | return SteviaLayoutXAxisAnchor(anchor: left.anchor, constant: Double(-right)) 170 | } 171 | 172 | @available(iOS 9.0, *) 173 | @discardableResult 174 | public func + (left: SteviaLayoutYAxisAnchor, right: Int) -> SteviaLayoutYAxisAnchor { 175 | return SteviaLayoutYAxisAnchor(anchor: left.anchor, constant: Double(right)) 176 | } 177 | 178 | @available(iOS 9.0, *) 179 | @discardableResult 180 | public func - (left: SteviaLayoutYAxisAnchor, right: Int) -> SteviaLayoutYAxisAnchor { 181 | return SteviaLayoutYAxisAnchor(anchor: left.anchor, constant: Double(-right)) 182 | } 183 | 184 | @available(iOS 9.0, *) 185 | @discardableResult 186 | public func + (left: SteviaLayoutXAxisAnchor, right: Int) -> SteviaLayoutXAxisAnchor { 187 | return SteviaLayoutXAxisAnchor(anchor: left.anchor, constant: Double(right)) 188 | } 189 | 190 | @available(iOS 9.0, *) 191 | @discardableResult 192 | public func - (left: SteviaLayoutXAxisAnchor, right: Int) -> SteviaLayoutXAxisAnchor { 193 | return SteviaLayoutXAxisAnchor(anchor: left.anchor, constant: Double(-right)) 194 | } 195 | 196 | // UILayoutSupport 197 | 198 | @available(iOS 9.0, *) 199 | public extension UILayoutSupport { 200 | 201 | var Top: SteviaLayoutYAxisAnchor { 202 | return SteviaLayoutYAxisAnchor(anchor: topAnchor) 203 | } 204 | 205 | var Bottom: SteviaLayoutYAxisAnchor { 206 | return SteviaLayoutYAxisAnchor(anchor: bottomAnchor) 207 | } 208 | } 209 | #endif 210 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Notifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Notifications.swift 3 | // LoginStevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 12/10/15. 6 | // Copyright © 2015 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public extension NSObject { 13 | 14 | func on(_ event: String, _ callback:@escaping @Sendable() -> Void) { 15 | _ = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: event), 16 | object: nil, 17 | queue: nil) { _ in 18 | callback() 19 | } 20 | } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Operators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Operators.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 10/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | prefix operator | 13 | @discardableResult 14 | @MainActor public prefix func | (p: UIView) -> UIView { 15 | p.leading(0) 16 | } 17 | 18 | postfix operator | 19 | @discardableResult 20 | @MainActor public postfix func | (p: UIView) -> UIView { 21 | p.trailing(0) 22 | } 23 | 24 | infix operator ~ : HeightPrecedence 25 | 26 | precedencegroup HeightPrecedence { 27 | lowerThan: AdditionPrecedence 28 | } 29 | 30 | @discardableResult 31 | @MainActor public func ~ (left: UIView, right: Double) -> UIView { 32 | left.height(right) 33 | } 34 | 35 | @discardableResult 36 | @MainActor public func ~ (left: UIView, right: CGFloat) -> UIView { 37 | left ~ Double(right) 38 | } 39 | 40 | @discardableResult 41 | @MainActor public func ~ (left: UIView, right: Int) -> UIView { 42 | left ~ Double(right) 43 | } 44 | 45 | @discardableResult 46 | @MainActor public func ~ (left: UIView, right: SteviaPercentage) -> UIView { 47 | left.height(right) 48 | } 49 | 50 | @discardableResult 51 | @MainActor public func ~ (left: UIView, right: SteviaFlexibleMargin) -> UIView { 52 | left.height(right) 53 | } 54 | 55 | @discardableResult 56 | @MainActor public func ~ (left: [UIView], right: Double) -> [UIView] { 57 | for l in left { l.height(right) } 58 | return left 59 | } 60 | 61 | @discardableResult 62 | @MainActor public func ~ (left: [UIView], right: CGFloat) -> [UIView] { 63 | left ~ Double(right) 64 | } 65 | 66 | @discardableResult 67 | @MainActor public func ~ (left: [UIView], right: Int) -> [UIView] { 68 | left ~ Double(right) 69 | } 70 | 71 | @discardableResult 72 | @MainActor public func ~ (left: [UIView], right: SteviaFlexibleMargin) -> [UIView] { 73 | for l in left { l.height(right) } 74 | return left 75 | } 76 | 77 | prefix operator |- 78 | @discardableResult 79 | public prefix func |- (p: Double) -> SideConstraint { 80 | var s = SideConstraint() 81 | s.constant = p 82 | return s 83 | } 84 | 85 | @discardableResult 86 | public prefix func |- (p: CGFloat) -> SideConstraint { 87 | |-Double(p) 88 | } 89 | 90 | @discardableResult 91 | public prefix func |- (p: Int) -> SideConstraint { 92 | |-Double(p) 93 | } 94 | 95 | @discardableResult 96 | @MainActor public prefix func |- (v: UIView) -> UIView { 97 | v.leading(8) 98 | } 99 | 100 | postfix operator -| 101 | @discardableResult 102 | public postfix func -| (p: Double) -> SideConstraint { 103 | var s = SideConstraint() 104 | s.constant = p 105 | return s 106 | } 107 | 108 | @discardableResult 109 | public postfix func -| (p: CGFloat) -> SideConstraint { 110 | Double(p)-| 111 | } 112 | 113 | @discardableResult 114 | public postfix func -| (p: Int) -> SideConstraint { 115 | Double(p)-| 116 | } 117 | 118 | @discardableResult 119 | @MainActor public postfix func -| (v: UIView) -> UIView { 120 | v.trailing(8) 121 | } 122 | 123 | public struct SideConstraint { 124 | var constant: Double! 125 | } 126 | 127 | public struct PartialConstraint { 128 | var view1: UIView! 129 | var constant: Double! 130 | var views: [UIView]? 131 | } 132 | 133 | @discardableResult 134 | public func - (left: UIView, right: Double) -> PartialConstraint { 135 | var p = PartialConstraint() 136 | p.view1 = left 137 | p.constant = right 138 | return p 139 | } 140 | 141 | @discardableResult 142 | public func - (left: UIView, right: CGFloat) -> PartialConstraint { 143 | left-Double(right) 144 | } 145 | 146 | @discardableResult 147 | public func - (left: UIView, right: Int) -> PartialConstraint { 148 | left-Double(right) 149 | } 150 | 151 | // Side Constraints 152 | 153 | @discardableResult 154 | @MainActor public func - (left: SideConstraint, right: UIView) -> UIView { 155 | if let spv = right.superview { 156 | let c = constraint(item: right, attribute: .leading, 157 | toItem: spv, attribute: .leading, 158 | constant: left.constant) 159 | spv.addConstraint(c) 160 | } 161 | return right 162 | } 163 | 164 | @discardableResult 165 | @MainActor public func - (left: [UIView], right: SideConstraint) -> [UIView] { 166 | let lastView = left[left.count-1] 167 | if let spv = lastView.superview { 168 | let c = constraint(item: lastView, attribute: .trailing, 169 | toItem: spv, attribute: .trailing, 170 | constant: -right.constant) 171 | spv.addConstraint(c) 172 | } 173 | return left 174 | } 175 | 176 | @discardableResult 177 | @MainActor public func - (left: UIView, right: SideConstraint) -> UIView { 178 | if let spv = left.superview { 179 | let c = constraint(item: left, attribute: .trailing, 180 | toItem: spv, attribute: .trailing, 181 | constant: -right.constant) 182 | spv.addConstraint(c) 183 | } 184 | return left 185 | } 186 | 187 | @discardableResult 188 | @MainActor public func - (left: PartialConstraint, right: UIView) -> [UIView] { 189 | if let views = left.views { 190 | if let spv = right.superview { 191 | let lastView = views[views.count-1] 192 | let c = constraint(item: lastView, attribute: .trailing, 193 | toItem: right, attribute: .leading, 194 | constant: -left.constant) 195 | spv.addConstraint(c) 196 | } 197 | 198 | return views + [right] 199 | } else { 200 | // were at the end?? nooope?/? 201 | if let spv = right.superview { 202 | let c = constraint(item: left.view1, attribute: .trailing, 203 | toItem: right, attribute: .leading, 204 | constant: -left.constant) 205 | spv.addConstraint(c) 206 | } 207 | return [left.view1, right] 208 | } 209 | } 210 | 211 | @discardableResult 212 | @MainActor public func - (left: UIView, right: UIView) -> [UIView] { 213 | if let spv = left.superview { 214 | let c = constraint(item: right, attribute: .leading, 215 | toItem: left, attribute: .trailing, 216 | constant: 8) 217 | spv.addConstraint(c) 218 | } 219 | return [left, right] 220 | } 221 | 222 | @discardableResult 223 | public func - (left: [UIView], right: Double) -> PartialConstraint { 224 | var p = PartialConstraint() 225 | p.constant = right 226 | p.views = left 227 | return p 228 | } 229 | 230 | @discardableResult 231 | public func - (left: [UIView], right: CGFloat) -> PartialConstraint { 232 | left-Double(right) 233 | } 234 | 235 | @discardableResult 236 | public func - (left: [UIView], right: Int) -> PartialConstraint { 237 | left-Double(right) 238 | } 239 | 240 | 241 | @discardableResult 242 | @MainActor public func - (left: [UIView], right: UIView) -> [UIView] { 243 | let lastView = left[left.count-1] 244 | if let spv = lastView.superview { 245 | let c = constraint(item: lastView, attribute: .trailing, 246 | toItem: right, attribute: .leading, 247 | constant: -8) 248 | spv.addConstraint(c) 249 | } 250 | return left + [right] 251 | } 252 | 253 | //// Test space in Horizointal layout "" 254 | public struct Space { 255 | var previousViews: [UIView]! 256 | } 257 | 258 | @discardableResult 259 | public func - (left: UIView, right: String) -> Space { 260 | Space(previousViews: [left]) 261 | } 262 | 263 | @discardableResult 264 | public func - (left: [UIView], right: String) -> Space { 265 | Space(previousViews: left) 266 | } 267 | 268 | @discardableResult 269 | public func - (left: Space, right: UIView) -> [UIView] { 270 | var va = left.previousViews 271 | va?.append(right) 272 | return va! 273 | } 274 | #endif 275 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Percentage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Percentage.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 21/01/2017. 6 | // Copyright © 2017 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public struct SteviaPercentage { 13 | let value: Double 14 | } 15 | 16 | postfix operator % 17 | public postfix func % (v: Double) -> SteviaPercentage { 18 | SteviaPercentage(value: Double(v)) 19 | } 20 | 21 | public postfix func % (v: CGFloat) -> SteviaPercentage { 22 | Double(v)% 23 | } 24 | 25 | public postfix func % (v: Int) -> SteviaPercentage { 26 | Double(v)% 27 | } 28 | 29 | public extension UIView { 30 | 31 | /** 32 | Adds an Autolayout constraint to provide the aspect ratio for the view. 33 | 34 | ``` 35 | image.aspectRatio(3/2) 36 | image.aspectRatio(150%) 37 | 38 | // is equivalent to 39 | 40 | image.Width == image.Height * 1.5 41 | image.Width == 150 % image.Height 42 | ``` 43 | 44 | - Returns: Itself, enabling chaining, 45 | 46 | */ 47 | @discardableResult 48 | func aspectRatio(_ p: SteviaPercentage) -> Self { 49 | Width == p.value % Height 50 | return self 51 | } 52 | 53 | /** 54 | Adds an Autolayout constraint for sizing the view. 55 | 56 | ``` 57 | image.size(100) 58 | image.size(100%) 59 | 60 | // is equivalent to 61 | 62 | image.width(100).height(100) 63 | ``` 64 | 65 | - Returns: Itself, enabling chaining, 66 | 67 | */ 68 | @discardableResult 69 | func size(_ p: SteviaPercentage) -> Self { 70 | width(p) 71 | height(p) 72 | return self 73 | } 74 | 75 | /** 76 | Adds an Autolayout constraint for setting the view's width. 77 | 78 | ``` 79 | image.width(100) 80 | image.width(<=100) 81 | image.width(>=100) 82 | image.width(100%) 83 | ``` 84 | 85 | - Returns: Itself, enabling chaining, 86 | 87 | */ 88 | @discardableResult 89 | func width(_ p: SteviaPercentage) -> Self { 90 | if let spv = superview { 91 | Width == p.value % spv.Width 92 | } 93 | return self 94 | } 95 | 96 | /** 97 | Adds an Autolayout constraint for setting the view's height. 98 | 99 | ``` 100 | image.height(100) 101 | 102 | // is equivalent to 103 | 104 | image ~ 100 105 | 106 | // Flexible margins 107 | image.height(<=100) 108 | image.height(>=100) 109 | image.height(100%) 110 | ``` 111 | 112 | - Returns: Itself, enabling chaining, 113 | 114 | */ 115 | @discardableResult 116 | func height(_ p: SteviaPercentage) -> Self { 117 | if let spv = superview { 118 | Height == p.value % spv.Height 119 | } 120 | return self 121 | } 122 | 123 | /** Sets the top margin for a view. 124 | 125 | Example Usage : 126 | 127 | label.top(20) 128 | label.top(<=20) 129 | label.top(>=20) 130 | label.top(20%) 131 | 132 | - Returns: Itself for chaining purposes 133 | */ 134 | @discardableResult 135 | func top(_ p: SteviaPercentage) -> Self { 136 | if let spv = superview { 137 | Top == p.value % spv.Bottom 138 | } 139 | return self 140 | } 141 | 142 | /** Sets the left margin for a view. 143 | 144 | Example Usage : 145 | 146 | label.left(20) 147 | label.left(<=20) 148 | label.left(>=20) 149 | label.left(20%) 150 | 151 | - Returns: Itself for chaining purposes 152 | */ 153 | @discardableResult 154 | func left(_ p: SteviaPercentage) -> Self { 155 | if let spv = superview { 156 | Left == p.value % spv.Right 157 | } 158 | return self 159 | } 160 | 161 | /** Sets the right margin for a view. 162 | 163 | Example Usage : 164 | 165 | label.right(20) 166 | label.right(<=20) 167 | label.right(>=20) 168 | label.right(20%) 169 | 170 | - Returns: Itself for chaining purposes 171 | */ 172 | @discardableResult 173 | func right(_ p: SteviaPercentage) -> Self { 174 | if let spv = superview { 175 | if p.value == 100 { 176 | Right == spv.Left 177 | } else { 178 | Right == (100 - p.value) % spv.Right 179 | } 180 | } 181 | return self 182 | } 183 | 184 | /** Sets the bottom margin for a view. 185 | 186 | Example Usage : 187 | 188 | label.bottom(20) 189 | label.bottom(<=20) 190 | label.bottom(>=20) 191 | label.bottom(20%) 192 | 193 | - Returns: Itself for chaining purposes 194 | */ 195 | @discardableResult 196 | func bottom(_ p: SteviaPercentage) -> Self { 197 | if let spv = superview { 198 | if p.value == 100 { 199 | Bottom == spv.Top 200 | } else { 201 | Bottom == (100 - p.value) % spv.Bottom 202 | } 203 | } 204 | return self 205 | } 206 | 207 | /** Sets the leading margin for a view. 208 | 209 | Example Usage : 210 | 211 | label.leading(20) 212 | label.leading(<=20) 213 | label.leading(>=20) 214 | label.leading(20%) 215 | 216 | - Returns: itself for chaining purposes 217 | */ 218 | @discardableResult 219 | func leading(_ p: SteviaPercentage) -> UIView { 220 | // Percent based (multipliers) with leading or trailing attributes 221 | // are not available so we introduce an intermediary layout guide. 222 | // |-[layoutGuide(== x % width)-[view] 223 | // RTL version: [view]-[layoutGuide(== x % width)-| 224 | if let spv = superview { 225 | let lg = UILayoutGuide() 226 | spv.addLayoutGuide(lg) 227 | let constraints = [ 228 | lg.widthAnchor.constraint(equalTo: spv.widthAnchor, multiplier: CGFloat(p.value / 100.0)), 229 | lg.leadingAnchor.constraint(equalTo: spv.leadingAnchor), 230 | leadingAnchor.constraint(equalTo: lg.trailingAnchor) 231 | ] 232 | constraints.forEach { $0.isActive = true } 233 | } 234 | return self 235 | } 236 | 237 | /** Sets the trailing margin for a view. 238 | 239 | Example Usage : 240 | 241 | label.trailing(20) 242 | label.trailing(<=20) 243 | label.trailing(>=20) 244 | label.trailing(20%) 245 | 246 | - Returns: itself for chaining purposes 247 | */ 248 | @discardableResult 249 | func trailing(_ p: SteviaPercentage) -> UIView { 250 | 251 | // Percent based (multipliers) with leading or trailing attributes 252 | // are not available so we introduce an intermediary layout guide. 253 | // |-[layoutGuide(== x % width)-[view] 254 | // RTL version: [view]-[layoutGuide(== x % width)-| 255 | if let spv = superview { 256 | let lg = UILayoutGuide() 257 | spv.addLayoutGuide(lg) 258 | let constraints = [ 259 | lg.widthAnchor.constraint(equalTo: spv.widthAnchor, multiplier: CGFloat(p.value / Double(100))), 260 | lg.trailingAnchor.constraint(equalTo: spv.trailingAnchor), 261 | trailingAnchor.constraint(equalTo: lg.leadingAnchor) 262 | ] 263 | constraints.forEach { $0.isActive = true } 264 | } 265 | return self 266 | } 267 | } 268 | #endif 269 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Position.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Position.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 10/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public extension UIView { 13 | 14 | /** Sets the left margin for a view. 15 | 16 | Example Usage : 17 | 18 | label.left(20) 19 | label.left(<=20) 20 | label.left(>=20) 21 | label.left(20%) 22 | 23 | - Returns: Itself for chaining purposes 24 | */ 25 | @discardableResult 26 | func left(_ points: Double) -> Self { 27 | position(.left, points: points) 28 | } 29 | 30 | /** Sets the left margin for a view. 31 | 32 | Example Usage : 33 | 34 | label.left(20) 35 | label.left(<=20) 36 | label.left(>=20) 37 | label.left(20%) 38 | 39 | - Returns: Itself for chaining purposes 40 | */ 41 | @discardableResult 42 | func left(_ points: CGFloat) -> Self { 43 | left(Double(points)) 44 | } 45 | 46 | /** Sets the left margin for a view. 47 | 48 | Example Usage : 49 | 50 | label.left(20) 51 | label.left(<=20) 52 | label.left(>=20) 53 | label.left(20%) 54 | 55 | - Returns: Itself for chaining purposes 56 | */ 57 | @discardableResult 58 | func left(_ points: Int) -> Self { 59 | left(Double(points)) 60 | } 61 | 62 | /** Sets the right margin for a view. 63 | 64 | Example Usage : 65 | 66 | label.right(20) 67 | label.right(<=20) 68 | label.right(>=20) 69 | label.right(20%) 70 | 71 | - Returns: Itself for chaining purposes 72 | */ 73 | @discardableResult 74 | func right(_ points: Double) -> Self { 75 | position(.right, points: -points) 76 | } 77 | 78 | /** Sets the right margin for a view. 79 | 80 | Example Usage : 81 | 82 | label.right(20) 83 | label.right(<=20) 84 | label.right(>=20) 85 | label.right(20%) 86 | 87 | - Returns: Itself for chaining purposes 88 | */ 89 | @discardableResult 90 | func right(_ points: CGFloat) -> Self { 91 | right(Double(points)) 92 | } 93 | 94 | /** Sets the right margin for a view. 95 | 96 | Example Usage : 97 | 98 | label.right(20) 99 | label.right(<=20) 100 | label.right(>=20) 101 | label.right(20%) 102 | 103 | - Returns: Itself for chaining purposes 104 | */ 105 | @discardableResult 106 | func right(_ points: Int) -> Self { 107 | right(Double(points)) 108 | } 109 | 110 | /** Sets the top margin for a view. 111 | 112 | Example Usage : 113 | 114 | label.top(20) 115 | label.top(<=20) 116 | label.top(>=20) 117 | label.top(20%) 118 | 119 | - Returns: Itself for chaining purposes 120 | */ 121 | @discardableResult 122 | func top(_ points: Double) -> Self { 123 | position(.top, points: points) 124 | } 125 | 126 | /** Sets the top margin for a view. 127 | 128 | Example Usage : 129 | 130 | label.top(20) 131 | label.top(<=20) 132 | label.top(>=20) 133 | label.top(20%) 134 | 135 | - Returns: Itself for chaining purposes 136 | */ 137 | @discardableResult 138 | func top(_ points: CGFloat) -> Self { 139 | top(Double(points)) 140 | } 141 | 142 | /** Sets the top margin for a view. 143 | 144 | Example Usage : 145 | 146 | label.top(20) 147 | label.top(<=20) 148 | label.top(>=20) 149 | label.top(20%) 150 | 151 | - Returns: Itself for chaining purposes 152 | */ 153 | @discardableResult 154 | func top(_ points: Int) -> Self { 155 | top(Double(points)) 156 | } 157 | 158 | /** Sets the bottom margin for a view. 159 | 160 | Example Usage : 161 | 162 | label.bottom(20) 163 | label.bottom(<=20) 164 | label.bottom(>=20) 165 | label.bottom(20%) 166 | 167 | - Returns: Itself for chaining purposes 168 | */ 169 | @discardableResult 170 | func bottom(_ points: Double) -> Self { 171 | position(.bottom, points: -points) 172 | } 173 | 174 | /** Sets the bottom margin for a view. 175 | 176 | Example Usage : 177 | 178 | label.bottom(20) 179 | label.bottom(<=20) 180 | label.bottom(>=20) 181 | label.bottom(20%) 182 | 183 | - Returns: Itself for chaining purposes 184 | */ 185 | @discardableResult 186 | func bottom(_ points: CGFloat) -> Self { 187 | bottom(Double(points)) 188 | } 189 | 190 | /** Sets the bottom margin for a view. 191 | 192 | Example Usage : 193 | 194 | label.bottom(20) 195 | label.bottom(<=20) 196 | label.bottom(>=20) 197 | label.bottom(20%) 198 | 199 | - Returns: Itself for chaining purposes 200 | */ 201 | @discardableResult 202 | func bottom(_ points: Int) -> Self { 203 | bottom(Double(points)) 204 | } 205 | 206 | /** Sets the left margin for a view. 207 | 208 | Example Usage : 209 | 210 | label.left(20) 211 | label.left(<=20) 212 | label.left(>=20) 213 | label.left(20%) 214 | 215 | - Returns: Itself for chaining purposes 216 | */ 217 | @discardableResult 218 | func left(_ fm: SteviaFlexibleMargin) -> Self { 219 | position(.left, relatedBy: fm.relation, points: fm.points) 220 | } 221 | 222 | /** Sets the right margin for a view. 223 | 224 | Example Usage : 225 | 226 | label.right(20) 227 | label.right(<=20) 228 | label.right(>=20) 229 | label.right(20%) 230 | 231 | - Returns: Itself for chaining purposes 232 | */ 233 | @discardableResult 234 | func right(_ fm: SteviaFlexibleMargin) -> Self { 235 | // For right this should be inverted. 236 | var n = SteviaFlexibleMargin() 237 | n.points = -fm.points 238 | if fm.relation == .greaterThanOrEqual { 239 | n.relation = .lessThanOrEqual 240 | } 241 | if fm.relation == .lessThanOrEqual { 242 | n.relation = .greaterThanOrEqual 243 | } 244 | return position(.right, relatedBy: n.relation, points: n.points) 245 | } 246 | 247 | /** Sets the top margin for a view. 248 | 249 | Example Usage : 250 | 251 | label.top(20) 252 | label.top(<=20) 253 | label.top(>=20) 254 | label.top(20%) 255 | 256 | - Returns: Itself for chaining purposes 257 | */ 258 | @discardableResult 259 | func top(_ fm: SteviaFlexibleMargin) -> Self { 260 | position(.top, relatedBy: fm.relation, points: fm.points) 261 | } 262 | 263 | /** Sets the bottom margin for a view. 264 | 265 | Example Usage : 266 | 267 | label.bottom(20) 268 | label.bottom(<=20) 269 | label.bottom(>=20) 270 | label.bottom(20%) 271 | 272 | - Returns: Itself for chaining purposes 273 | */ 274 | @discardableResult 275 | func bottom(_ fm: SteviaFlexibleMargin) -> Self { 276 | // For bottom this should be inverted. 277 | var n = SteviaFlexibleMargin() 278 | n.points = -fm.points 279 | if fm.relation == .greaterThanOrEqual { 280 | n.relation = .lessThanOrEqual 281 | } 282 | if fm.relation == .lessThanOrEqual { 283 | n.relation = .greaterThanOrEqual 284 | } 285 | return position(.bottom, relatedBy: n.relation, points: n.points) 286 | } 287 | 288 | /** Sets the leading margin for a view. 289 | 290 | Example Usage : 291 | 292 | label.leading(20) 293 | label.leading(<=20) 294 | label.leading(>=20) 295 | label.leading(20%) 296 | 297 | - Returns: itself for chaining purposes 298 | */ 299 | 300 | @discardableResult 301 | func leading(_ points: Double) -> UIView { 302 | position(.leading, points: points) 303 | } 304 | 305 | /** Sets the leading margin for a view. 306 | 307 | Example Usage : 308 | 309 | label.leading(20) 310 | label.leading(<=20) 311 | label.leading(>=20) 312 | label.leading(20%) 313 | 314 | - Returns: itself for chaining purposes 315 | */ 316 | @discardableResult 317 | func leading(_ points: CGFloat) -> UIView { 318 | leading(Double(points)) 319 | } 320 | 321 | /** Sets the leading margin for a view. 322 | 323 | Example Usage : 324 | 325 | label.leading(20) 326 | label.leading(<=20) 327 | label.leading(>=20) 328 | label.leading(20%) 329 | 330 | - Returns: itself for chaining purposes 331 | */ 332 | @discardableResult 333 | func leading(_ points: Int) -> UIView { 334 | leading(Double(points)) 335 | } 336 | 337 | @discardableResult 338 | func leading(_ fm: SteviaFlexibleMargin) -> UIView { 339 | position(.leading, relatedBy: fm.relation, points: fm.points) 340 | } 341 | 342 | /** Sets the trailing margin for a view. 343 | 344 | Example Usage : 345 | 346 | label.trailing(20) 347 | label.trailing(<=20) 348 | label.trailing(>=20) 349 | label.trailing(20%) 350 | 351 | - Returns: itself for chaining purposes 352 | */ 353 | @discardableResult 354 | func trailing(_ points: Double) -> UIView { 355 | position(.trailing, points: -points) 356 | } 357 | 358 | /** Sets the trailing margin for a view. 359 | 360 | Example Usage : 361 | 362 | label.trailing(20) 363 | label.trailing(<=20) 364 | label.trailing(>=20) 365 | label.trailing(20%) 366 | 367 | - Returns: itself for chaining purposes 368 | */ 369 | @discardableResult 370 | func trailing(_ points: CGFloat) -> UIView { 371 | trailing(Double(points)) 372 | } 373 | 374 | /** Sets the trailing margin for a view. 375 | 376 | Example Usage : 377 | 378 | label.trailing(20) 379 | label.trailing(<=20) 380 | label.trailing(>=20) 381 | label.trailing(20%) 382 | 383 | - Returns: itself for chaining purposes 384 | */ 385 | @discardableResult 386 | func trailing(_ points: Int) -> UIView { 387 | trailing(Double(points)) 388 | } 389 | 390 | @discardableResult 391 | func trailing(_ fm: SteviaFlexibleMargin) -> UIView { 392 | var invertedRelation = fm.relation 393 | if invertedRelation == .lessThanOrEqual { 394 | invertedRelation = .greaterThanOrEqual 395 | } else if invertedRelation == .greaterThanOrEqual { 396 | invertedRelation = .lessThanOrEqual 397 | } 398 | return position(.trailing, relatedBy: invertedRelation!, points: -fm.points) 399 | } 400 | 401 | fileprivate func position(_ position: NSLayoutConstraint.Attribute, 402 | relatedBy: NSLayoutConstraint.Relation = .equal, 403 | points: Double) -> Self { 404 | if let spv = superview { 405 | let c = constraint(item: self, attribute: position, 406 | relatedBy: relatedBy, 407 | toItem: spv, 408 | constant: points) 409 | spv.addConstraint(c) 410 | } 411 | return self 412 | } 413 | } 414 | #endif 415 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Size.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Size.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 10/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public extension UIView { 13 | 14 | /** 15 | Adds an Autolayout constraint to provide the aspect ratio for the view. 16 | 17 | ``` 18 | image.aspectRatio(3.0/2.0) 19 | image.aspectRatio(150%) 20 | 21 | // is equivalent to 22 | 23 | image.Width == image.Height * 1.5 24 | image.Width == 150 % image.Height 25 | ``` 26 | 27 | - Returns: Itself, enabling chaining, 28 | 29 | */ 30 | @discardableResult 31 | func aspectRatio(_ ratio: Double = 1) -> Self { 32 | Width == Height * ratio 33 | return self 34 | } 35 | 36 | /** 37 | Adds an Autolayout constraint to provide the aspect ratio for the view. 38 | 39 | ``` 40 | image.aspectRatio(3.0/2.0) 41 | image.aspectRatio(150%) 42 | 43 | // is equivalent to 44 | 45 | image.Width == image.Height * 1.5 46 | image.Width == 150 % image.Height 47 | ``` 48 | 49 | - Returns: Itself, enabling chaining, 50 | 51 | */ 52 | @discardableResult 53 | func aspectRatio(_ ratio: CGFloat = 1) -> Self { 54 | aspectRatio(Double(ratio)) 55 | return self 56 | } 57 | 58 | /** 59 | Adds an Autolayout constraint to provide the aspect ratio for the view. 60 | 61 | ``` 62 | image.aspectRatio(3.0/2.0) 63 | image.aspectRatio(150%) 64 | 65 | // is equivalent to 66 | 67 | image.Width == image.Height * 1.5 68 | image.Width == 150 % image.Height 69 | ``` 70 | 71 | - Returns: Itself, enabling chaining, 72 | 73 | */ 74 | @discardableResult 75 | func aspectRatio(_ ratio: Int = 1) -> Self { 76 | aspectRatio(Double(ratio)) 77 | return self 78 | } 79 | 80 | /** 81 | Adds an Autolayout constraint for sizing the view. 82 | 83 | ``` 84 | image.size(100) 85 | image.size(100%) 86 | 87 | // is equivalent to 88 | 89 | image.width(100).height(100) 90 | ``` 91 | 92 | - Returns: Itself, enabling chaining, 93 | 94 | */ 95 | @discardableResult 96 | func size(_ points: Double) -> Self { 97 | width(points) 98 | height(points) 99 | return self 100 | } 101 | 102 | /** 103 | Adds an Autolayout constraint for sizing the view. 104 | 105 | ``` 106 | image.size(100) 107 | image.size(100%) 108 | 109 | // is equivalent to 110 | 111 | image.width(100).height(100) 112 | ``` 113 | 114 | - Returns: Itself, enabling chaining, 115 | 116 | */ 117 | @discardableResult 118 | func size(_ points: CGFloat) -> Self { 119 | size(Double(points)) 120 | } 121 | 122 | /** 123 | Adds an Autolayout constraint for sizing the view. 124 | 125 | ``` 126 | image.size(100) 127 | image.size(100%) 128 | 129 | // is equivalent to 130 | 131 | image.width(100).height(100) 132 | ``` 133 | 134 | - Returns: Itself, enabling chaining, 135 | 136 | */ 137 | @discardableResult 138 | func size(_ points: Int) -> Self { 139 | size(Double(points)) 140 | } 141 | 142 | /** 143 | Adds an Autolayout constraint for setting the view's height. 144 | 145 | ``` 146 | image.height(100) 147 | 148 | // is equivalent to 149 | 150 | image ~ 100 151 | 152 | // Flexible margins 153 | image.height(<=100) 154 | image.height(>=100) 155 | image.height(100%) 156 | ``` 157 | 158 | - Returns: Itself, enabling chaining, 159 | 160 | */ 161 | @discardableResult 162 | func height(_ points: Double) -> Self { 163 | size(.height, points: points) 164 | } 165 | 166 | /** 167 | Adds an Autolayout constraint for setting the view's height. 168 | 169 | ``` 170 | image.height(100) 171 | 172 | // is equivalent to 173 | 174 | image ~ 100 175 | 176 | // Flexible margins 177 | image.height(<=100) 178 | image.height(>=100) 179 | image.height(100%) 180 | ``` 181 | 182 | - Returns: Itself, enabling chaining, 183 | 184 | */ 185 | @discardableResult 186 | func height(_ points: CGFloat) -> Self { 187 | height(Double(points)) 188 | } 189 | 190 | /** 191 | Adds an Autolayout constraint for setting the view's height. 192 | 193 | ``` 194 | image.height(100) 195 | 196 | // is equivalent to 197 | 198 | image ~ 100 199 | 200 | // Flexible margins 201 | image.height(<=100) 202 | image.height(>=100) 203 | image.height(100%) 204 | ``` 205 | 206 | - Returns: Itself, enabling chaining, 207 | 208 | */ 209 | @discardableResult 210 | func height(_ points: Int) -> Self { 211 | height(Double(points)) 212 | } 213 | 214 | /** 215 | Adds an Autolayout constraint for setting the view's width. 216 | 217 | ``` 218 | image.width(100) 219 | image.width(<=100) 220 | image.width(>=100) 221 | image.width(100%) 222 | ``` 223 | 224 | - Returns: Itself, enabling chaining, 225 | 226 | */ 227 | @discardableResult 228 | func width(_ points: Double) -> Self { 229 | size(.width, points: points) 230 | } 231 | 232 | /** 233 | Adds an Autolayout constraint for setting the view's width. 234 | 235 | ``` 236 | image.width(100) 237 | image.width(<=100) 238 | image.width(>=100) 239 | image.width(100%) 240 | ``` 241 | 242 | - Returns: Itself, enabling chaining, 243 | 244 | */ 245 | @discardableResult 246 | func width(_ points: CGFloat) -> Self { 247 | width(Double(points)) 248 | } 249 | 250 | /** 251 | Adds an Autolayout constraint for setting the view's width. 252 | 253 | ``` 254 | image.width(100) 255 | image.width(<=100) 256 | image.width(>=100) 257 | image.width(100%) 258 | ``` 259 | 260 | - Returns: Itself, enabling chaining, 261 | 262 | */ 263 | @discardableResult 264 | func width(_ points: Int) -> Self { 265 | width(Double(points)) 266 | } 267 | 268 | /** 269 | Adds an Autolayout constraint for setting the view's height. 270 | 271 | ``` 272 | image.height(100) 273 | 274 | // is equivalent to 275 | 276 | image ~ 100 277 | 278 | // Flexible margins 279 | image.height(<=100) 280 | image.height(>=100) 281 | image.height(100%) 282 | ``` 283 | 284 | - Returns: Itself, enabling chaining, 285 | 286 | */ 287 | @discardableResult 288 | func height(_ fm: SteviaFlexibleMargin) -> Self { 289 | return size(.height, relatedBy: fm.relation, points: fm.points) 290 | } 291 | 292 | /** 293 | Adds an Autolayout constraint for setting the view's width. 294 | 295 | ``` 296 | image.width(100) 297 | image.width(<=100) 298 | image.width(>=100) 299 | image.width(100%) 300 | ``` 301 | 302 | - Returns: Itself, enabling chaining, 303 | 304 | */ 305 | @discardableResult 306 | func width(_ fm: SteviaFlexibleMargin) -> Self { 307 | return size(.width, relatedBy: fm.relation, points: fm.points) 308 | } 309 | 310 | fileprivate func size(_ attribute: NSLayoutConstraint.Attribute, 311 | relatedBy: NSLayoutConstraint.Relation = .equal, 312 | points: Double) -> Self { 313 | let c = constraint(item: self, 314 | attribute: attribute, 315 | relatedBy: relatedBy, 316 | constant: points) 317 | if let spv = superview { 318 | spv.addConstraint(c) 319 | } else { 320 | addConstraint(c) 321 | } 322 | return self 323 | } 324 | } 325 | 326 | /** 327 | Enforces an array of views to keep the same size. 328 | 329 | ``` 330 | equal(sizes: image1, image2, image3) 331 | ``` 332 | 333 | - Returns: The views enabling chaining. 334 | 335 | */ 336 | @discardableResult 337 | @MainActor public func equal(sizes views: UIView...) -> [UIView] { 338 | return equal(sizes: views) 339 | } 340 | 341 | @available(*, deprecated, renamed:"equal(sizes:)") 342 | @discardableResult 343 | @MainActor public func equalSizes(_ views: UIView...) -> [UIView] { 344 | return equal(sizes: views) 345 | } 346 | 347 | /** 348 | Enforces an array of views to keep the same size. 349 | 350 | ``` 351 | equal(sizes: image1, image2, image3) 352 | ``` 353 | 354 | - Returns: The views enabling chaining. 355 | 356 | */ 357 | @discardableResult 358 | @MainActor public func equal(sizes views: [UIView]) -> [UIView] { 359 | equal(heights: views) 360 | equal(widths: views) 361 | return views 362 | } 363 | 364 | @available(*, deprecated, renamed:"equal(sizes:)") 365 | @discardableResult 366 | @MainActor public func equalSizes(_ views: [UIView]) -> [UIView] { 367 | equal(heights: views) 368 | equal(widths: views) 369 | return views 370 | } 371 | 372 | /** 373 | Enforces an array of views to keep the same widths. 374 | 375 | ``` 376 | equal(widths: image1, image2, image3) 377 | ``` 378 | 379 | - Returns: The views enabling chaining. 380 | 381 | */ 382 | @discardableResult 383 | @MainActor public func equal(widths views: UIView...) -> [UIView] { 384 | return equal(widths: views) 385 | } 386 | 387 | @available(*, deprecated, renamed:"equal(widths:)") 388 | @discardableResult 389 | @MainActor public func equalWidths(_ views: UIView...) -> [UIView] { 390 | return equal(widths: views) 391 | } 392 | 393 | /** 394 | Enforces an array of views to keep the same widths. 395 | 396 | ``` 397 | equal(widths: image1, image2, image3) 398 | ``` 399 | 400 | - Returns: The views enabling chaining. 401 | 402 | */ 403 | @discardableResult 404 | @MainActor public func equal(widths views: [UIView]) -> [UIView] { 405 | equal(.width, views: views) 406 | return views 407 | } 408 | 409 | @available(*, deprecated, renamed:"equal(widths:)") 410 | @discardableResult 411 | @MainActor public func equalWidths(_ views: [UIView]) -> [UIView] { 412 | equal(.width, views: views) 413 | return views 414 | } 415 | 416 | /** 417 | Enforces an array of views to keep the same heights. 418 | 419 | ``` 420 | equal(heights: image1, image2, image3) 421 | ``` 422 | 423 | - Returns: The views enabling chaining. 424 | 425 | */ 426 | @discardableResult 427 | @MainActor public func equal(heights views: UIView...) -> [UIView] { 428 | return equal(heights: views) 429 | } 430 | 431 | @available(*, deprecated, renamed:"equal(heights:)") 432 | @discardableResult 433 | @MainActor public func equalHeights(_ views: UIView...) -> [UIView] { 434 | return equal(heights: views) 435 | } 436 | 437 | /** 438 | Enforces an array of views to keep the same heights. 439 | 440 | ``` 441 | equal(heights: image1, image2, image3) 442 | ``` 443 | 444 | - Returns: The views enabling chaining. 445 | 446 | */ 447 | @discardableResult 448 | @MainActor public func equal(heights views: [UIView]) -> [UIView] { 449 | equal(.height, views: views) 450 | return views 451 | } 452 | 453 | @available(*, deprecated, renamed:"equal(heights:)") 454 | @discardableResult 455 | @MainActor public func equalHeights(_ views: [UIView]) -> [UIView] { 456 | equal(.height, views: views) 457 | return views 458 | } 459 | 460 | @MainActor private func equal(_ attribute: NSLayoutConstraint.Attribute, views: [UIView]) { 461 | var previousView: UIView? 462 | for v in views { 463 | if let pv = previousView { 464 | if let spv = v.superview { 465 | spv.addConstraint(item: v, attribute: attribute, toItem: pv) 466 | } 467 | } 468 | previousView = v 469 | } 470 | } 471 | #endif 472 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Stacks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Stacks.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 10/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | //enum SteviaLayoutItemType { 13 | // 14 | //} 15 | 16 | public protocol SteviaLayoutItem { 17 | var any: Any {get} 18 | } 19 | 20 | extension SteviaLayoutItem { 21 | public var any: Any { self } 22 | } 23 | extension UIView: SteviaLayoutItem {} 24 | extension Int: SteviaLayoutItem {} 25 | extension Double: SteviaLayoutItem {} 26 | extension CGFloat: SteviaLayoutItem {} 27 | extension String: SteviaLayoutItem {} 28 | 29 | extension FlexibleSpace: SteviaLayoutItem {} 30 | extension SteviaFlexibleMargin: SteviaLayoutItem {} 31 | extension SteviaPercentage: SteviaLayoutItem {} 32 | extension Array: SteviaLayoutItem where Element: UIView {} 33 | 34 | public struct FlexibleSpace { 35 | public init() {} 36 | } 37 | 38 | @resultBuilder public struct SteviaLayoutBuilder { 39 | public static func buildBlock(_ content: SteviaLayoutItem...) -> [SteviaLayoutItem] { 40 | return content 41 | } 42 | } 43 | 44 | public extension UIView { 45 | @discardableResult 46 | func layout(@SteviaLayoutBuilder content: () -> [SteviaLayoutItem]) -> UIView { 47 | let subviews = content() 48 | let anys = subviews.map { $0.any } 49 | layout(anys) 50 | return self 51 | } 52 | } 53 | 54 | public extension UIView { 55 | 56 | /** 57 | 58 | Lays out the views on both axis. 59 | 60 | Note that this is not needed for Horizontal only layouts. 61 | 62 | `layout` is primarily for laying out views vertically but horizontal statements 63 | are supported, making it perfect for describing a layout in one single statement. 64 | 65 | ``` 66 | layout( 67 | 100, 68 | |-email-| ~ 80, 69 | 8, 70 | |-password-forgot-| ~ 80, 71 | >=20, 72 | |login| ~ 80, 73 | 0 74 | ) 75 | ``` 76 | */ 77 | // @available(*, deprecated, message: "Use Layout { } function builder instead.") 78 | @discardableResult 79 | func layout(_ objects: Any...) -> [UIView] { 80 | return layout(objects) 81 | } 82 | 83 | @discardableResult 84 | func layout(_ objects: [Any]) -> [UIView] { 85 | var previousMargin: Double? 86 | var previousFlexibleMargin: SteviaFlexibleMargin? 87 | var previousPercentMargin: SteviaPercentage? 88 | 89 | for (i, o) in objects.enumerated() { 90 | 91 | func viewLogic(_ v: UIView) { 92 | if let pm = previousMargin { 93 | if i == 1 { 94 | v.top(pm) // only if first view 95 | } else { 96 | if let vx = objects[i-2] as? UIView { 97 | vx.stackV(m: pm, v: v) 98 | } else if let va = objects[i-2] as? [UIView] { 99 | va.first!.stackV(m: pm, v: v) 100 | } 101 | } 102 | previousMargin = nil 103 | } else if let pfm = previousFlexibleMargin { 104 | if i == 1 { 105 | v.top(pfm) // only if first view 106 | } else { 107 | if let vx = objects[i-2] as? UIView { 108 | addConstraint( 109 | item: v, attribute: .top, 110 | relatedBy: pfm.relation, 111 | toItem: vx, attribute: .bottom, 112 | multiplier: 1, constant: pfm.points 113 | ) 114 | } else if let va = objects[i-2] as? [UIView] { 115 | addConstraint( 116 | item: v, attribute: .top, 117 | relatedBy: pfm.relation, 118 | toItem: va.first!, attribute: .bottom, 119 | multiplier: 1, constant: pfm.points 120 | ) 121 | } 122 | } 123 | previousFlexibleMargin = nil 124 | } else if let ppm = previousPercentMargin { 125 | if i == 1 { 126 | v.top(ppm) // only if first view 127 | } else { 128 | if let vx = objects[i-2] as? UIView { 129 | // Add layout guide to suport %-based spaces. 130 | let percent = ppm.value / 100 131 | 132 | let lg = UILayoutGuide() 133 | addLayoutGuide(lg) 134 | NSLayoutConstraint.activate([ 135 | lg.topAnchor.constraint(equalTo: vx.bottomAnchor), 136 | lg.heightAnchor.constraint(equalTo: heightAnchor, multiplier: CGFloat(percent)), 137 | v.topAnchor.constraint(equalTo: lg.bottomAnchor) 138 | ]) 139 | } 140 | } 141 | previousPercentMargin = nil 142 | } else { 143 | tryStackViewVerticallyWithPreviousView(v, index: i, objects: objects) 144 | } 145 | } 146 | 147 | switch o { 148 | case let v as UIView: 149 | viewLogic(v) 150 | case is Int, is Double, is CGFloat: 151 | let m = doubleMarginFromObject(o) 152 | previousMargin = m // Store margin for next pass 153 | if i != 0 && i == (objects.count - 1) { 154 | //Last Margin, Bottom 155 | if let previousView = objects[i-1] as? UIView { 156 | previousView.bottom(m) 157 | } else if let va = objects[i-1] as? [UIView] { 158 | va.first!.bottom(m) 159 | } 160 | } 161 | case let fm as SteviaFlexibleMargin: 162 | previousFlexibleMargin = fm // Store margin for next pass 163 | if i != 0 && i == (objects.count - 1) { 164 | //Last Margin, Bottom 165 | if let previousView = objects[i-1] as? UIView { 166 | previousView.bottom(fm) 167 | } else if let va = objects[i-1] as? [UIView] { 168 | va.first!.bottom(fm) 169 | } 170 | } 171 | case let pm as SteviaPercentage: 172 | previousPercentMargin = pm // Store margin for next pass 173 | if i != 0 && i == (objects.count - 1) { 174 | //Last Margin, Bottom 175 | if let previousView = objects[i-1] as? UIView { 176 | previousView.bottom(pm) 177 | } else if let va = objects[i-1] as? [UIView] { 178 | va.first!.bottom(pm) 179 | } 180 | } 181 | case _ as String:() //Do nothin' ! 182 | case let a as [UIView]: 183 | align(horizontally: a) 184 | let v = a.first! 185 | viewLogic(v) 186 | default: () 187 | } 188 | } 189 | return objects.map {$0 as? UIView }.compactMap {$0} 190 | } 191 | 192 | fileprivate func doubleMarginFromObject(_ o: Any) -> Double { 193 | var m: Double = 0 194 | if let i = o as? Int { 195 | m = Double(i) 196 | } else if let d = o as? Double { 197 | m = d 198 | } else if let cg = o as? CGFloat { 199 | m = Double(cg) 200 | } 201 | return m 202 | } 203 | 204 | fileprivate func tryStackViewVerticallyWithPreviousView(_ view: UIView, 205 | index: Int, objects: [Any]) { 206 | if let pv = previousViewFromIndex(index, objects: objects) { 207 | pv.stackV(v: view) 208 | } 209 | } 210 | 211 | fileprivate func previousViewFromIndex(_ index: Int, objects: [Any]) -> UIView? { 212 | if index != 0 { 213 | if let previousView = objects[index-1] as? UIView { 214 | return previousView 215 | } 216 | } 217 | return nil 218 | } 219 | 220 | @discardableResult 221 | fileprivate func stackV(m points: Double = 0, v: UIView) -> UIView { 222 | return stack(.vertical, points: points, v: v) 223 | } 224 | 225 | fileprivate func stack(_ axis: NSLayoutConstraint.Axis, 226 | points: Double = 0, v: UIView) -> UIView { 227 | let a: NSLayoutConstraint.Attribute = axis == .vertical ? .top : .left 228 | let b: NSLayoutConstraint.Attribute = axis == .vertical ? .bottom : .right 229 | if let spv = superview { 230 | let c = constraint(item: v, attribute: a, toItem: self, attribute: b, constant: points) 231 | spv.addConstraint(c) 232 | } 233 | return v 234 | } 235 | } 236 | #endif 237 | -------------------------------------------------------------------------------- /Sources/Stevia/Stevia+Style.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stevia+Style.swift 3 | // LoginStevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 01/10/15. 6 | // Copyright © 2015 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | 12 | public extension UIAppearance { 13 | 14 | /** Applies a styling block on an element. 15 | 16 | Example Usage: 17 | 18 | ``` 19 | button.style { b in 20 | b.A = X 21 | b.B = Y 22 | b.C = Z 23 | } 24 | ``` 25 | 26 | Handy for reusing styles : 27 | ``` 28 | button.style(buttonStyle) 29 | 30 | // later 31 | func buttonStyle(b: UIButton) { 32 | ..styling code 33 | } 34 | ``` 35 | 36 | - Returns: Itself for chaining purposes 37 | 38 | */ 39 | @discardableResult 40 | func style(_ styleClosure: (Self) -> Void) -> Self { 41 | styleClosure(self) 42 | return self 43 | } 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /SteviaLayout.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'SteviaLayout' 3 | s.version = "5.1.2" 4 | s.summary = "Elegant view layout for iOS" 5 | s.homepage = "https://github.com/freshOS/Stevia" 6 | s.license = { :type => "MIT", :file => "LICENSE" } 7 | s.author = 'S4cha' 8 | s.source = { :git => "https://github.com/freshOS/Stevia.git", 9 | :tag => s.version.to_s } 10 | s.social_media_url = 'https://twitter.com/sachadso' 11 | s.source_files = "Sources/Stevia/*.swift" 12 | s.ios.deployment_target = "9" 13 | s.tvos.deployment_target = "10.2" 14 | s.description = "Elegant view layout for iOS :leaves: - Auto layout code finally readable by a human being" 15 | s.module_name = 'Stevia' 16 | end 17 | -------------------------------------------------------------------------------- /Tests/SteviaTests/BaselineTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaselineTests.swift 3 | // SteviaTests 4 | // 5 | // Created by Sacha on 09/09/2018. 6 | // Copyright © 2018 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Stevia 11 | import Testing 12 | 13 | @Suite 14 | @MainActor struct BaselineTests { 15 | 16 | let win = UIWindow(frame: UIScreen.main.bounds) 17 | let ctrler = UIViewController() 18 | let label1 = UILabel() 19 | let label2 = UILabel() 20 | 21 | init() { 22 | win.rootViewController = ctrler 23 | ctrler.view.subviews { 24 | label1 25 | label2 26 | } 27 | } 28 | 29 | @Test 30 | func alignLastBaselines() { 31 | label1.top(100) 32 | align(lastBaselines: label1, label2) 33 | ctrler.view.layoutIfNeeded() 34 | #expect(label1.forLastBaselineLayout.frame.minY == label2.forLastBaselineLayout.frame.minY) 35 | } 36 | 37 | @Test 38 | func alignFirstBaselines() { 39 | label1.top(100) 40 | align(firstBaselines: label1, label2) 41 | ctrler.view.layoutIfNeeded() 42 | #expect(label1.forLastBaselineLayout.frame.minY == label2.forLastBaselineLayout.frame.minY) 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Tests/SteviaTests/CenterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CenterTests.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 21/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import Testing 10 | import Numerics 11 | import UIKit 12 | import Stevia 13 | 14 | 15 | @Suite 16 | @MainActor struct CenterTests { 17 | 18 | let win = UIWindow(frame: UIScreen.main.bounds) 19 | var ctrler = UIViewController() 20 | var v = UIView() 21 | 22 | init() { 23 | win.rootViewController = ctrler 24 | ctrler.view.subviews { v } 25 | v.size(100) 26 | ctrler.view.layoutIfNeeded() 27 | } 28 | 29 | @Test 30 | func centerHorizontally() { 31 | v.centerHorizontally() 32 | ctrler.view.setNeedsLayout() 33 | ctrler.view.layoutIfNeeded() 34 | #expect(v.frame.origin.y == 0) 35 | #expect(isApproximatelyEqual(v.frame.origin.x, ctrler.view.frame.width/2.0 - (v.frame.width/2.0))) 36 | #expect(v.frame.width == 100) 37 | #expect(v.frame.height == 100) 38 | } 39 | 40 | @Test 41 | func centerHorizontallyWithOffsetDouble() { 42 | v.centerHorizontally(offset: Double(50)) 43 | ctrler.view.setNeedsLayout() 44 | ctrler.view.layoutIfNeeded() 45 | #expect(v.frame.origin.y == 0) 46 | let val = v.frame.origin.x - 50 47 | #expect(isApproximatelyEqual(val, ctrler.view.frame.width/2.0 - (v.frame.width/2.0))) 48 | #expect(v.frame.width == 100) 49 | #expect(v.frame.height == 100) 50 | } 51 | 52 | @Test 53 | func centerHorizontallyWithOffsetCGFloat() { 54 | v.centerHorizontally(offset: CGFloat(50)) 55 | ctrler.view.setNeedsLayout() 56 | ctrler.view.layoutIfNeeded() 57 | #expect(v.frame.origin.y == 0) 58 | let val = v.frame.origin.x - 50 59 | #expect(isApproximatelyEqual(val, (ctrler.view.frame.width/2.0 - (v.frame.width/2.0)))) 60 | #expect(v.frame.width == 100) 61 | #expect(v.frame.height == 100) 62 | } 63 | 64 | @Test 65 | func centerHorizontallyWithOffsetInt() { 66 | v.centerHorizontally(offset: Int(50)) 67 | ctrler.view.setNeedsLayout() 68 | ctrler.view.layoutIfNeeded() 69 | #expect(v.frame.origin.y == 0) 70 | let val = v.frame.origin.x - 50 71 | #expect(isApproximatelyEqual(val, (ctrler.view.frame.width/2.0 - (v.frame.width/2.0)))) 72 | #expect(v.frame.width == 100) 73 | #expect(v.frame.height == 100) 74 | } 75 | 76 | @Test 77 | func centerVertically() { 78 | v.centerVertically() 79 | ctrler.view.setNeedsLayout() 80 | ctrler.view.layoutIfNeeded() 81 | #expect(v.frame.origin.y == ctrler.view.frame.height/2.0 - (v.frame.height/2.0)) 82 | #expect(v.frame.origin.x == 0) 83 | #expect(v.frame.width == 100) 84 | #expect(v.frame.height == 100) 85 | } 86 | 87 | @Test 88 | func centerVerticallyWithOffsetDouble() { 89 | v.centerVertically(offset: Double(50)) 90 | ctrler.view.setNeedsLayout() 91 | ctrler.view.layoutIfNeeded() 92 | let val = v.frame.origin.y - 50 93 | #expect(isApproximatelyEqual(val, (ctrler.view.frame.height/2.0 - (v.frame.height/2.0)))) 94 | #expect(v.frame.origin.x == 0) 95 | #expect(v.frame.width == 100) 96 | #expect(v.frame.height == 100) 97 | } 98 | 99 | @Test 100 | func centerVerticallyWithOffsetCGFloat() { 101 | v.centerVertically(offset: CGFloat(50)) 102 | ctrler.view.setNeedsLayout() 103 | ctrler.view.layoutIfNeeded() 104 | let val = v.frame.origin.y - 50 105 | #expect(isApproximatelyEqual(val, (ctrler.view.frame.height/2.0 - (v.frame.height/2.0)))) 106 | #expect(v.frame.origin.x == 0) 107 | #expect(v.frame.width == 100) 108 | #expect(v.frame.height == 100) 109 | } 110 | 111 | @Test 112 | func centerVerticallyWithOffsetInt() { 113 | v.centerVertically(offset: Int(50)) 114 | ctrler.view.setNeedsLayout() 115 | ctrler.view.layoutIfNeeded() 116 | 117 | let val = v.frame.origin.y - 50 118 | #expect(isApproximatelyEqual(val, (ctrler.view.frame.height/2.0 - (v.frame.height/2.0)))) 119 | #expect(v.frame.origin.x == 0) 120 | #expect(v.frame.width == 100) 121 | #expect(v.frame.height == 100) 122 | } 123 | 124 | @Test 125 | func centerInContainer() { 126 | v.centerInContainer() 127 | ctrler.view.setNeedsLayout() 128 | ctrler.view.layoutIfNeeded() 129 | 130 | #expect(v.frame.origin.y == ctrler.view.frame.height/2.0 - (v.frame.height/2.0)) 131 | #expect(isApproximatelyEqual(v.frame.origin.x, ctrler.view.frame.width/2.0 - (v.frame.width/2.0))) 132 | #expect(v.frame.width == 100) 133 | #expect(v.frame.height == 100) 134 | #expect(isApproximatelyEqual(v.frame.origin.y, ctrler.view.frame.height/2.0 - (v.frame.height/2.0))) 135 | #expect(isApproximatelyEqual(v.frame.origin.x, ctrler.view.frame.width/2.0 - (v.frame.width/2.0))) 136 | #expect(v.frame.height == 100) 137 | } 138 | } 139 | 140 | func isApproximatelyEqual(_ a: Double, _ b: Double) -> Bool { 141 | return a.isApproximatelyEqual(to: b, absoluteTolerance: 0.5) 142 | } 143 | -------------------------------------------------------------------------------- /Tests/SteviaTests/ContentTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentTests.swift 3 | // Stevia 4 | // 5 | // Created by Naabed on 12/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import Testing 10 | import UIKit 11 | import Stevia 12 | 13 | let title = "TitleTest" 14 | 15 | @Suite 16 | @MainActor struct UIButtonContentTests { 17 | 18 | var button = UIButton() 19 | 20 | init() { 21 | button = UIButton() 22 | } 23 | 24 | @Test 25 | func testText() { 26 | button.text(title) 27 | #expect(button.currentTitle == title) 28 | #expect(button.state == .normal) 29 | } 30 | 31 | @Test 32 | func testTextKey() { 33 | button.textKey(title) 34 | #expect(button.currentTitle == title) 35 | } 36 | } 37 | 38 | 39 | @Suite 40 | @MainActor struct UILabelContentTests { 41 | 42 | let label = UILabel() 43 | 44 | @Test 45 | func testText() { 46 | label.text(title) 47 | #expect(label.text == title) 48 | } 49 | 50 | @Test 51 | func testTextKey() { 52 | label.textKey(title) 53 | #expect(label.text == title) 54 | } 55 | } 56 | 57 | @Suite 58 | @MainActor struct UITextFieldContentTests { 59 | 60 | let textField = UITextField() 61 | 62 | @Test 63 | func testPlaceholder() { 64 | textField.placeholder(title) 65 | #expect(textField.placeholder == title) 66 | } 67 | } 68 | 69 | @Suite 70 | @MainActor struct UIImageViewContentTests { 71 | 72 | let imageView = UIImageView() 73 | 74 | @Test 75 | func testImage() { 76 | imageView.image("foo") 77 | //XCTAssertEqual(button.currentImage, title) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/SteviaTests/FillTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FillTests.swift 3 | // Stevia 4 | // 5 | // Created by Naabed on 12/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import Testing 10 | import UIKit 11 | import Stevia 12 | 13 | @Suite 14 | @MainActor class FillTests { 15 | 16 | let win = UIWindow(frame: UIScreen.main.bounds) 17 | let ctrler = UIViewController() 18 | 19 | init() { 20 | win.rootViewController = ctrler 21 | } 22 | 23 | @Test 24 | func testFillContainer() { 25 | let b = UIButton() 26 | ctrler.view.subviews { b } 27 | b.fillContainer() 28 | ctrler.view.layoutIfNeeded() // This is needed to force auto-layout to kick-in 29 | #expect(ctrler.view.frame == b.frame) 30 | } 31 | 32 | @Test 33 | func testFillContainerWithPaddingDouble() { 34 | let padding: Double = 10.0 35 | let b = UIButton() 36 | ctrler.view.subviews { b } 37 | b.fillContainer(padding: padding) 38 | ctrler.view.layoutIfNeeded() // This is needed to force auto-layout to kick-in 39 | #expect(ctrler.view.frame.height == b.frame.height + CGFloat(padding) * 2) 40 | #expect(ctrler.view.frame.width == b.frame.width + CGFloat(padding) * 2) 41 | } 42 | 43 | @Test 44 | func testFillContainerWithPaddingCGFloat() { 45 | let padding: CGFloat = 10.0 46 | let b = UIButton() 47 | ctrler.view.subviews { b } 48 | b.fillContainer(padding: padding) 49 | ctrler.view.layoutIfNeeded() // This is needed to force auto-layout to kick-in 50 | #expect(ctrler.view.frame.height == b.frame.height + CGFloat(padding) * 2) 51 | #expect(ctrler.view.frame.width == b.frame.width + CGFloat(padding) * 2) 52 | } 53 | 54 | @Test 55 | func testFillContainerWithPaddingInt() { 56 | let padding: Int = 10 57 | let b = UIButton() 58 | ctrler.view.subviews { b } 59 | b.fillContainer(padding: padding) 60 | ctrler.view.layoutIfNeeded() // This is needed to force auto-layout to kick-in 61 | #expect(ctrler.view.frame.height == b.frame.height + CGFloat(padding) * 2) 62 | #expect(ctrler.view.frame.width == b.frame.width + CGFloat(padding) * 2) 63 | } 64 | 65 | @Test 66 | func testFillVertically() { 67 | let b = UIButton() 68 | ctrler.view.subviews { b } 69 | b.width(10) 70 | b.fillVertically() 71 | ctrler.view.layoutIfNeeded() // This is needed to force auto-layout to kick-in 72 | #expect(ctrler.view.frame.height == b.frame.height) 73 | #expect(10 == b.frame.width) 74 | } 75 | 76 | @Test 77 | func testFillVerticallyWithPaddingDouble() { 78 | let padding: Double = 40.0 79 | let b = UIButton() 80 | ctrler.view.subviews { b } 81 | b.fillVertically(padding: padding) 82 | ctrler.view.layoutIfNeeded() // This is needed to force auto-layout to kick-in 83 | #expect(ctrler.view.frame.height == b.frame.height + CGFloat(padding) * 2) 84 | } 85 | 86 | @Test 87 | func testFillVerticallyWithPaddingCGFloat() { 88 | let padding: CGFloat = 30.0 89 | let b = UIButton() 90 | ctrler.view.subviews { b } 91 | b.fillVertically(padding: padding) 92 | ctrler.view.layoutIfNeeded() // This is needed to force auto-layout to kick-in 93 | #expect(ctrler.view.frame.height == b.frame.height + CGFloat(padding) * 2) 94 | } 95 | 96 | @Test 97 | func testFillVerticallyWithPaddingInt() { 98 | let padding: Int = 14 99 | let b = UIButton() 100 | ctrler.view.subviews { b } 101 | b.fillVertically(padding: padding) 102 | ctrler.view.layoutIfNeeded() // This is needed to force auto-layout to kick-in 103 | #expect(ctrler.view.frame.height == b.frame.height + CGFloat(padding) * 2) 104 | } 105 | 106 | @Test 107 | func testFillHorizontallyWithPaddingDouble() { 108 | let padding: Double = 40.0 109 | let b = UIButton() 110 | ctrler.view.subviews { b } 111 | b.fillHorizontally(padding: padding) 112 | ctrler.view.layoutIfNeeded() // This is needed to force auto-layout to kick-in 113 | #expect(ctrler.view.frame.width == b.frame.width + CGFloat(padding) * 2) 114 | } 115 | 116 | @Test 117 | func testFillHorizontallyWithPaddingCGFloat() { 118 | let padding: CGFloat = 30.0 119 | let b = UIButton() 120 | ctrler.view.subviews { b } 121 | b.fillHorizontally(padding: padding) 122 | ctrler.view.layoutIfNeeded() // This is needed to force auto-layout to kick-in 123 | #expect(ctrler.view.frame.width == b.frame.width + CGFloat(padding) * 2) 124 | } 125 | 126 | @Test 127 | func testFillHorizontallyWithPaddingInt() { 128 | let padding: Int = 14 129 | let b = UIButton() 130 | ctrler.view.subviews { b } 131 | b.fillHorizontally(padding: padding) 132 | ctrler.view.layoutIfNeeded() // This is needed to force auto-layout to kick-in 133 | #expect(ctrler.view.frame.width == b.frame.width + CGFloat(padding) * 2) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Tests/SteviaTests/FlexibleMarginTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlexibleMarginTests.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 21/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import Testing 10 | import UIKit 11 | import Stevia 12 | 13 | @Suite 14 | @MainActor struct FlexibleMarginTests { 15 | 16 | let win = UIWindow(frame: UIScreen.main.bounds) 17 | let ctrler = UIViewController() 18 | let v = UIView() 19 | 20 | init() { 21 | win.rootViewController = ctrler 22 | ctrler.view.subviews { v } 23 | v.size(100.0) 24 | } 25 | 26 | @Test 27 | func testGreaterTopDouble() { 28 | v.top(>=Double(23)) 29 | ctrler.view.layoutIfNeeded() 30 | #expect(v.frame.origin.y == 23) 31 | #expect(v.frame.origin.x == 0) 32 | #expect(v.frame.width == 100) 33 | #expect(v.frame.height == 100) 34 | } 35 | 36 | @Test 37 | func testGreaterTopCGFloat() { 38 | v.top(>=CGFloat(23)) 39 | ctrler.view.layoutIfNeeded() 40 | #expect(v.frame.origin.y == 23) 41 | #expect(v.frame.origin.x == 0) 42 | #expect(v.frame.width == 100) 43 | #expect(v.frame.height == 100) 44 | } 45 | 46 | @Test 47 | func testGreaterTopInt() { 48 | v.top(>=Int(23)) 49 | ctrler.view.layoutIfNeeded() 50 | #expect(v.frame.origin.y == 23) 51 | #expect(v.frame.origin.x == 0) 52 | #expect(v.frame.width == 100) 53 | #expect(v.frame.height == 100) 54 | } 55 | 56 | @Test 57 | func testGreaterBottom() { 58 | v.bottom(>=45) 59 | ctrler.view.layoutIfNeeded() 60 | #expect(v.frame.origin.y == ctrler.view.frame.height - v.frame.height - 45) 61 | #expect(v.frame.origin.x == 0) 62 | #expect(v.frame.width == 100) 63 | #expect(v.frame.height == 100) 64 | } 65 | 66 | @Test 67 | func testGreaterLeft() { 68 | v.left(>=23) 69 | ctrler.view.layoutIfNeeded() 70 | #expect(v.frame.origin.y == 0) 71 | #expect(v.frame.origin.x == 23) 72 | #expect(v.frame.width == 100) 73 | #expect(v.frame.height == 100) 74 | } 75 | 76 | @Test 77 | func testGreaterLeading() { 78 | v.leading(>=23) 79 | ctrler.view.layoutIfNeeded() 80 | #expect(v.frame.origin.y == 0) 81 | #expect(v.frame.origin.x == 23) 82 | #expect(v.frame.width == 100) 83 | #expect(v.frame.height == 100) 84 | } 85 | 86 | @Test 87 | func testGreaterLeadingRTL() { 88 | ctrler.view.semanticContentAttribute = .forceRightToLeft 89 | v.leading(>=23) 90 | ctrler.view.layoutIfNeeded() 91 | #expect(v.frame.origin.y == 0) 92 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 23) 93 | #expect(v.frame.width == 100) 94 | #expect(v.frame.height == 100) 95 | } 96 | 97 | @Test 98 | func testGreaterRight() { 99 | v.right(>=74) 100 | ctrler.view.layoutIfNeeded() 101 | #expect(v.frame.origin.y == 0) 102 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 74) 103 | #expect(v.frame.width == 100) 104 | #expect(v.frame.height == 100) 105 | } 106 | 107 | @Test 108 | func testGreaterTrailing() { 109 | v.trailing(>=74) 110 | ctrler.view.layoutIfNeeded() 111 | #expect(v.frame.origin.y == 0) 112 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 74) 113 | #expect(v.frame.width == 100) 114 | #expect(v.frame.height == 100) 115 | } 116 | 117 | @Test 118 | func testGreaterTrailingRTL() { 119 | ctrler.view.semanticContentAttribute = .forceRightToLeft 120 | v.trailing(>=74) 121 | ctrler.view.layoutIfNeeded() 122 | #expect(v.frame.origin.y == 0) 123 | #expect(v.frame.origin.x == 74) 124 | #expect(v.frame.width == 100) 125 | #expect(v.frame.height == 100) 126 | } 127 | 128 | @Test 129 | func testLessTopDouble() { 130 | v.top(<=Double(23)) 131 | ctrler.view.layoutIfNeeded() 132 | #expect(v.frame.origin.y == 23) 133 | #expect(v.frame.origin.x == 0) 134 | #expect(v.frame.width == 100) 135 | #expect(v.frame.height == 100) 136 | } 137 | 138 | @Test 139 | func testLessTopCGFloat() { 140 | v.top(<=CGFloat(23)) 141 | ctrler.view.layoutIfNeeded() 142 | #expect(v.frame.origin.y == 23) 143 | #expect(v.frame.origin.x == 0) 144 | #expect(v.frame.width == 100) 145 | #expect(v.frame.height == 100) 146 | } 147 | 148 | @Test 149 | func testLessTopInt() { 150 | v.top(<=Int(23)) 151 | ctrler.view.layoutIfNeeded() 152 | #expect(v.frame.origin.y == 23) 153 | #expect(v.frame.origin.x == 0) 154 | #expect(v.frame.width == 100) 155 | #expect(v.frame.height == 100) 156 | } 157 | 158 | @Test 159 | func testLessBottom() { 160 | v.bottom(<=45) 161 | ctrler.view.layoutIfNeeded() 162 | #expect(v.frame.origin.y == ctrler.view.frame.height - v.frame.height - 45) 163 | #expect(v.frame.origin.x == 0) 164 | #expect(v.frame.width == 100) 165 | #expect(v.frame.height == 100) 166 | } 167 | 168 | @Test 169 | func testLessLeft() { 170 | v.left(<=23) 171 | ctrler.view.layoutIfNeeded() 172 | #expect(v.frame.origin.y == 0) 173 | #expect(v.frame.origin.x == 23) 174 | #expect(v.frame.width == 100) 175 | #expect(v.frame.height == 100) 176 | } 177 | 178 | @Test 179 | func testLessLeading() { 180 | v.leading(<=23) 181 | ctrler.view.layoutIfNeeded() 182 | #expect(v.frame.origin.y == 0) 183 | #expect(v.frame.origin.x == 23) 184 | #expect(v.frame.width == 100) 185 | #expect(v.frame.height == 100) 186 | } 187 | 188 | @Test 189 | func testLessLeadingRTL() { 190 | ctrler.view.semanticContentAttribute = .forceRightToLeft 191 | v.leading(<=23) 192 | ctrler.view.layoutIfNeeded() 193 | #expect(v.frame.origin.y == 0) 194 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 23) 195 | #expect(v.frame.width == 100) 196 | #expect(v.frame.height == 100) 197 | } 198 | 199 | @Test 200 | func testLessLeftOperator() { 201 | |-(<=23)-v 202 | ctrler.view.layoutIfNeeded() 203 | #expect(v.frame.origin.y == 0) 204 | #expect(v.frame.origin.x == 23) 205 | #expect(v.frame.width == 100) 206 | #expect(v.frame.height == 100) 207 | } 208 | 209 | @Test 210 | func testLessLeftOperatorRTL() { 211 | ctrler.view.semanticContentAttribute = .forceRightToLeft 212 | |-(<=23)-v 213 | ctrler.view.layoutIfNeeded() 214 | #expect(v.frame.origin.y == 0) 215 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 23) 216 | #expect(v.frame.width == 100) 217 | #expect(v.frame.height == 100) 218 | } 219 | 220 | @Test 221 | func testLessRight() { 222 | v.right(<=74) 223 | ctrler.view.layoutIfNeeded() 224 | #expect(v.frame.origin.y == 0) 225 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 74) 226 | #expect(v.frame.width == 100) 227 | #expect(v.frame.height == 100) 228 | } 229 | 230 | @Test 231 | func testLessTrailing() { 232 | v.trailing(<=74) 233 | ctrler.view.layoutIfNeeded() 234 | #expect(v.frame.origin.y == 0) 235 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 74) 236 | #expect(v.frame.width == 100) 237 | #expect(v.frame.height == 100) 238 | } 239 | 240 | @Test 241 | func testLessTrailingRTL() { 242 | ctrler.view.semanticContentAttribute = .forceRightToLeft 243 | v.trailing(<=74) 244 | ctrler.view.layoutIfNeeded() 245 | #expect(v.frame.origin.y == 0) 246 | #expect(v.frame.origin.x == 74) 247 | #expect(v.frame.width == 100) 248 | #expect(v.frame.height == 100) 249 | } 250 | 251 | @Test 252 | func testLessRightOperator() { 253 | v-(<=74)-| 254 | ctrler.view.layoutIfNeeded() 255 | #expect(v.frame.origin.y == 0) 256 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 74) 257 | #expect(v.frame.width == 100) 258 | #expect(v.frame.height == 100) 259 | } 260 | 261 | @Test 262 | func testLessRightOperatorRTL() { 263 | ctrler.view.semanticContentAttribute = .forceRightToLeft 264 | v-(<=74)-| 265 | ctrler.view.layoutIfNeeded() 266 | #expect(v.frame.origin.y == 0) 267 | #expect(v.frame.origin.x == 74) 268 | #expect(v.frame.width == 100) 269 | #expect(v.frame.height == 100) 270 | } 271 | 272 | @Test 273 | func testMarginGreaterBetweenTwoViews() { 274 | let v1 = UIView() 275 | let v2 = UIView() 276 | v.removeFromSuperview() 277 | ctrler.view.subviews { v1; v2 } 278 | for view in ctrler.view.subviews { 279 | #expect(view.frame.origin.y == 0) 280 | #expect(view.frame.origin.x == 0) 281 | #expect(view.frame.width == 0) 282 | #expect(view.frame.height == 0) 283 | } 284 | 285 | |v1.width(10)-(>=25)-v2 286 | ctrler.view.layoutIfNeeded() 287 | #expect(v1.frame.origin.y == 0) 288 | #expect(v1.frame.origin.x == 0) 289 | #expect(v1.frame.width == 10) 290 | #expect(v1.frame.height == 0) 291 | 292 | #expect(v2.frame.origin.y == 0) 293 | #expect(v2.frame.origin.x == 35) 294 | #expect(v2.frame.width == 0) 295 | #expect(v2.frame.height == 0) 296 | } 297 | 298 | @Test 299 | func testMarginGreaterBetweenTwoViewsRTL() { 300 | ctrler.view.semanticContentAttribute = .forceRightToLeft 301 | let v1 = UIView() 302 | let v2 = UIView() 303 | v.removeFromSuperview() 304 | ctrler.view.subviews { 305 | v1 306 | v2 307 | } 308 | for view in ctrler.view.subviews { 309 | #expect(view.frame.origin.y == 0) 310 | #expect(view.frame.origin.x == 0) 311 | #expect(view.frame.width == 0) 312 | #expect(view.frame.height == 0) 313 | } 314 | 315 | |v1.width(10)-(>=25)-v2 316 | ctrler.view.layoutIfNeeded() 317 | #expect(v1.frame.origin.y == 0) 318 | #expect(v1.frame.origin.x == ctrler.view.frame.width - 10) 319 | #expect(v1.frame.width == 10) 320 | #expect(v1.frame.height == 0) 321 | 322 | #expect(v2.frame.origin.y == 0) 323 | #expect(v2.frame.origin.x == ctrler.view.frame.width - v1.frame.width - 25) 324 | #expect(v2.frame.width == 0) 325 | #expect(v2.frame.height == 0) 326 | 327 | } 328 | 329 | @Test 330 | func testMarginLesserBetweenTwoViews() { 331 | let v1 = UIView() 332 | let v2 = UIView() 333 | v.removeFromSuperview() 334 | ctrler.view.subviews { 335 | v1 336 | v2 337 | } 338 | for view in ctrler.view.subviews { 339 | #expect(view.frame.origin.y == 0) 340 | #expect(view.frame.origin.x == 0) 341 | #expect(view.frame.width == 0) 342 | #expect(view.frame.height == 0) 343 | } 344 | 345 | |v1.width(10)-(<=25)-v2 346 | ctrler.view.layoutIfNeeded() 347 | #expect(v1.frame.origin.y == 0) 348 | #expect(v1.frame.origin.x == 0) 349 | #expect(v1.frame.width == 10) 350 | #expect(v1.frame.height == 0) 351 | 352 | #expect(v2.frame.origin.y == 0) 353 | #expect(v2.frame.origin.x == 35) 354 | #expect(v2.frame.width == 0) 355 | #expect(v2.frame.height == 0) 356 | } 357 | 358 | @Test 359 | func testMarginLesserBetweenTwoViewsRTL() { 360 | ctrler.view.semanticContentAttribute = .forceRightToLeft 361 | let v1 = UIView() 362 | let v2 = UIView() 363 | v.removeFromSuperview() 364 | ctrler.view.subviews { 365 | v1 366 | v2 367 | } 368 | for view in ctrler.view.subviews { 369 | #expect(view.frame.origin.y == 0) 370 | #expect(view.frame.origin.x == 0) 371 | #expect(view.frame.width == 0) 372 | #expect(view.frame.height == 0) 373 | } 374 | 375 | |v1.width(10)-(<=25)-v2 376 | ctrler.view.layoutIfNeeded() 377 | #expect(v1.frame.origin.y == 0) 378 | #expect(v1.frame.origin.x == ctrler.view.frame.width - 10) 379 | #expect(v1.frame.width == 10) 380 | #expect(v1.frame.height == 0) 381 | 382 | #expect(v2.frame.origin.y == 0) 383 | #expect(v2.frame.origin.x == ctrler.view.frame.width - v1.frame.width - 25) 384 | #expect(v2.frame.width == 0) 385 | #expect(v2.frame.height == 0) 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /Tests/SteviaTests/FullLayoutTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FullLayoutTests.swift 3 | // Stevia 4 | // 5 | // Created by Sacha Durand Saint Omer on 21/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import Testing 10 | import UIKit 11 | import Stevia 12 | 13 | class TestView: UIView { 14 | 15 | let email = UITextField() 16 | let password = UITextField() 17 | let login = UIButton() 18 | 19 | let view1 = UIView() 20 | let view2 = UIView() 21 | 22 | convenience init() { 23 | self.init(frame: CGRect.zero) 24 | 25 | subviews { 26 | email 27 | password 28 | login 29 | } 30 | 31 | layout { 32 | 100.5 33 | |-email-22-| ~ 10% 34 | 20 35 | |password.width(54) ~ 47.0 36 | >=0 37 | login.centerHorizontally() ~ 99 38 | 7 39 | } 40 | 41 | subviews { 42 | view1 43 | view2 44 | } 45 | 46 | layout { 47 | 10% 48 | |view1| ~ 20 49 | 33% 50 | |view2| 51 | 20% 52 | } 53 | 54 | } 55 | } 56 | 57 | @Suite 58 | @MainActor class FullLayoutTests { 59 | 60 | let win = UIWindow(frame: UIScreen.main.bounds) 61 | let vc = UIViewController() 62 | let v = TestView() 63 | 64 | init() { 65 | win.rootViewController = vc 66 | v.frame = vc.view.frame 67 | vc.view = v 68 | } 69 | 70 | @Test 71 | func testFullLayout() { 72 | #expect(vc.view.frame.origin.x == 0) 73 | #expect(vc.view.frame.origin.y == 0) 74 | #expect(vc.view.frame.width == win.frame.width) 75 | #expect(vc.view.frame.height == win.frame.height) 76 | 77 | v.layoutIfNeeded() 78 | 79 | // Email 80 | #expect(isApproximatelyEqual(v.email.frame.origin.y, 100.5)) 81 | #expect(v.email.frame.origin.x == 8) 82 | #expect(v.email.frame.width == win.frame.width - 8 - 22) 83 | #expect(isApproximatelyEqual(v.email.frame.height, win.frame.height*0.1)) 84 | 85 | // Password 86 | #expect(isApproximatelyEqual(v.password.frame.origin.y, v.email.frame.origin.y+v.email.frame.height + 20)) 87 | #expect(v.password.frame.origin.x == 0) 88 | #expect(v.password.frame.width == 54) 89 | #expect(v.password.frame.height == 47) 90 | 91 | // Password 92 | #expect(v.login.frame.origin.y == win.frame.height - v.login.frame.height - 7) 93 | #expect(isApproximatelyEqual(v.login.frame.origin.x, win.frame.width/2.0 - (v.login.frame.width/2.0))) 94 | #expect(v.login.frame.height == 99) 95 | } 96 | 97 | @Test 98 | func testFullLayoutRTL() { 99 | vc.view.semanticContentAttribute = .forceRightToLeft 100 | #expect(vc.view.frame.origin.x == 0) 101 | #expect(vc.view.frame.origin.y == 0) 102 | #expect(vc.view.frame.width == win.frame.width) 103 | #expect(vc.view.frame.height == win.frame.height) 104 | 105 | v.layoutIfNeeded() 106 | 107 | // Email 108 | #expect(isApproximatelyEqual(v.email.frame.origin.y, 100.5)) 109 | #expect(v.email.frame.origin.x == 22) 110 | #expect(v.email.frame.width == win.frame.width - 8 - 22) 111 | #expect(isApproximatelyEqual(v.email.frame.height, win.frame.height*0.1)) 112 | 113 | // Password 114 | #expect(isApproximatelyEqual(v.password.frame.origin.y, v.email.frame.origin.y+v.email.frame.height + 20)) 115 | #expect(v.password.frame.origin.x == vc.view.frame.width - 54) 116 | #expect(v.password.frame.width == 54) 117 | #expect(v.password.frame.height == 47) 118 | 119 | // Password 120 | #expect(v.login.frame.origin.y == win.frame.height - v.login.frame.height - 7) 121 | #expect(isApproximatelyEqual(v.login.frame.origin.x, win.frame.width/2.0 - (v.login.frame.width/2.0))) 122 | #expect(v.login.frame.height == 99) 123 | } 124 | 125 | @Test 126 | func testPercentLayout() { 127 | #expect(vc.view.frame.origin.x == 0) 128 | #expect(vc.view.frame.origin.y == 0) 129 | #expect(vc.view.frame.width == win.frame.width) 130 | #expect(vc.view.frame.height == win.frame.height) 131 | 132 | v.layoutIfNeeded() 133 | 134 | #expect(isApproximatelyEqual(v.view1.frame.origin.y, v.frame.height*0.1)) 135 | #expect(v.view1.frame.origin.x == 0) 136 | #expect(v.view1.frame.width == v.frame.width) 137 | #expect(isApproximatelyEqual(v.view2.frame.origin.y, (v.frame.height*0.1) + 20 + (v.frame.height*0.33))) 138 | #expect(v.view2.frame.origin.x == 0) 139 | #expect(isApproximatelyEqual(v.view2.frame.origin.y + v.view2.frame.height, (v.frame.height*0.8))) 140 | #expect(v.view2.frame.width == v.frame.width) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Tests/SteviaTests/GetConstraintsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetConstraintsTests.swift 3 | // Stevia 4 | // 5 | // Created by Sacha DSO on 14/06/2017. 6 | // Copyright © 2017 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import Testing 10 | import UIKit 11 | import Stevia 12 | 13 | @Suite 14 | @MainActor struct GetConstraintsTests { 15 | 16 | let v = UIView() 17 | let spv = UIView() 18 | 19 | init() { 20 | spv.subviews { v } 21 | } 22 | 23 | @Test 24 | func canGetLeftConstraint() throws { 25 | #expect(v.leftConstraint == nil) 26 | v.left(10) 27 | let c = try #require(v.leftConstraint) 28 | #expect(c.constant == 10) 29 | #expect(c.firstItem as? UIView == v) 30 | #expect(c.secondItem as? UIView == spv) 31 | #expect(c.firstAttribute == .left) 32 | #expect(c.secondAttribute == .left) 33 | #expect(c.multiplier == 1) 34 | #expect(c.relation == .equal) 35 | #expect(c.priority.rawValue == 751) 36 | #expect(c.isActive) 37 | } 38 | 39 | @Test 40 | func canGetRightConstraint() throws { 41 | #expect(v.rightConstraint == nil) 42 | v.right(42) 43 | let c = try #require(v.rightConstraint) 44 | #expect(c.constant == -42) 45 | #expect(c.firstItem as? UIView == v) 46 | #expect(c.secondItem as? UIView == spv) 47 | #expect(c.firstAttribute == .right) 48 | #expect(c.secondAttribute == .right) 49 | #expect(c.multiplier == 1) 50 | #expect(c.relation == .equal) 51 | #expect(c.priority.rawValue == 751) 52 | #expect(c.isActive) 53 | } 54 | 55 | @Test 56 | func canGetTopConstraint() throws { 57 | #expect(v.topConstraint == nil) 58 | v.top(23) 59 | let c = try #require(v.topConstraint) 60 | #expect(c.constant == 23) 61 | #expect(c.firstItem as? UIView == v) 62 | #expect(c.secondItem as? UIView == spv) 63 | #expect(c.firstAttribute == .top) 64 | #expect(c.secondAttribute == .top) 65 | #expect(c.multiplier == 1) 66 | #expect(c.relation == .equal) 67 | #expect(c.priority.rawValue == 751) 68 | #expect(c.isActive) 69 | } 70 | 71 | @Test 72 | func testCanGetBottomConstraint() throws { 73 | #expect(v.bottomConstraint == nil) 74 | v.bottom(145) 75 | let c = try #require(v.bottomConstraint) 76 | #expect(c.constant == -145) 77 | #expect(c.firstItem as? UIView == v) 78 | #expect(c.secondItem as? UIView == spv) 79 | #expect(c.firstAttribute == .bottom) 80 | #expect(c.secondAttribute == .bottom) 81 | #expect(c.multiplier == 1) 82 | #expect(c.relation == .equal) 83 | #expect(c.priority.rawValue == 751) 84 | #expect(c.isActive) 85 | } 86 | 87 | @Test 88 | func testCanGetHeightConstraint() throws { 89 | #expect(v.heightConstraint == nil) 90 | v.height(35) 91 | let c = try #require(v.heightConstraint) 92 | #expect(c.constant == 35) 93 | #expect(c.firstItem as? UIView == v) 94 | #expect(c.secondItem == nil) 95 | #expect(c.firstAttribute == .height) 96 | #expect(c.secondAttribute == .notAnAttribute) 97 | #expect(c.multiplier == 1) 98 | #expect(c.relation == .equal) 99 | #expect(c.priority.rawValue == 751) 100 | #expect(c.isActive) 101 | } 102 | 103 | @Test 104 | func canGetWidthConstraint() throws { 105 | #expect(v.widthConstraint == nil) 106 | v.width(51) 107 | let c = try #require(v.widthConstraint) 108 | #expect(c.constant == 51) 109 | #expect(c.firstItem as? UIView == v) 110 | #expect(c.secondItem == nil) 111 | #expect(c.firstAttribute == .width) 112 | #expect(c.secondAttribute == .notAnAttribute) 113 | #expect(c.multiplier == 1) 114 | #expect(c.relation == .equal) 115 | #expect(c.priority.rawValue == 751) 116 | #expect(c.isActive) 117 | } 118 | 119 | @Test 120 | func canGetTrailingConstraint() throws { 121 | #expect(v.trailingConstraint == nil) 122 | v.trailingAnchor.constraint(equalTo: spv.trailingAnchor, constant: 104).isActive = true 123 | let c = try #require(v.trailingConstraint) 124 | #expect(c.constant == 104) 125 | #expect(c.firstItem as? UIView == v) 126 | #expect(c.secondItem as? UIView == spv) 127 | #expect(c.firstAttribute == .trailing) 128 | #expect(c.secondAttribute == .trailing) 129 | #expect(c.multiplier == 1) 130 | #expect(c.relation == .equal) 131 | #expect(c.priority.rawValue == 1000) 132 | #expect(c.isActive) 133 | } 134 | 135 | @Test 136 | func canGetLeadingonstraint() throws { 137 | #expect(v.leadingConstraint == nil) 138 | v.leadingAnchor.constraint(equalTo: spv.leadingAnchor, constant: 73).isActive = true 139 | let c = try #require(v.leadingConstraint) 140 | #expect(c.constant == 73) 141 | #expect(c.firstItem as? UIView == v) 142 | #expect(c.secondItem as? UIView == spv) 143 | #expect(c.firstAttribute == .leading) 144 | #expect(c.secondAttribute == .leading) 145 | #expect(c.multiplier == 1) 146 | #expect(c.relation == .equal) 147 | #expect(c.priority.rawValue == 1000) 148 | #expect(c.isActive) 149 | } 150 | 151 | @Test 152 | func canGetCenterXConstraint() throws { 153 | #expect(v.centerXConstraint == nil) 154 | v.CenterX == spv.CenterX + 27 155 | let c = try #require(v.centerXConstraint) 156 | #expect(c.constant == 27) 157 | #expect(c.firstItem as? UIView == v) 158 | #expect(c.secondItem as? UIView == spv) 159 | #expect(c.firstAttribute == .centerX) 160 | #expect(c.secondAttribute == .centerX) 161 | #expect(c.multiplier == 1) 162 | #expect(c.relation == .equal) 163 | #expect(c.priority.rawValue == 751) 164 | #expect(c.isActive) 165 | } 166 | 167 | @Test 168 | func canGetCenterYConstraint() throws { 169 | #expect(v.centerYConstraint == nil) 170 | v.CenterY == spv.CenterY - 32 171 | let c = try #require(v.centerYConstraint) 172 | #expect(c.constant == -32) 173 | #expect(c.firstItem as? UIView == v) 174 | #expect(c.secondItem as? UIView == spv) 175 | #expect(c.firstAttribute == .centerY) 176 | #expect(c.secondAttribute == .centerY) 177 | #expect(c.multiplier == 1) 178 | #expect(c.relation == .equal) 179 | #expect(c.priority.rawValue == 751) 180 | #expect(c.isActive) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Tests/SteviaTests/HierarchyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HierarchyTests.swift 3 | // Stevia 4 | // 5 | // Created by Naabed on 12/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import Testing 10 | import UIKit 11 | import Stevia 12 | 13 | @Suite 14 | @MainActor 15 | struct HierarchyTests { 16 | 17 | @Test 18 | func subviews() { 19 | let view = UIView() 20 | let v1 = UIView() 21 | let v2 = UIView() 22 | view.subviews { 23 | v1 24 | v2 25 | } 26 | #expect(view.subviews.count == 2) 27 | #expect(view.subviews.contains(v1)) 28 | #expect(view.subviews.contains(v2)) 29 | #expect(v1.translatesAutoresizingMaskIntoConstraints == false) 30 | #expect(v2.translatesAutoresizingMaskIntoConstraints == false) 31 | } 32 | 33 | @Test 34 | func tableViewCellSubviews() { 35 | let cell = UITableViewCell() 36 | let v1 = UIView() 37 | let v2 = UIView() 38 | cell.subviews { 39 | v1 40 | v2 41 | } 42 | 43 | #expect(cell.contentView.subviews.count == 2) 44 | #expect(cell.contentView.subviews.contains(v1)) 45 | #expect(cell.contentView.subviews.contains(v2)) 46 | #expect(v1.translatesAutoresizingMaskIntoConstraints == false) 47 | #expect(v2.translatesAutoresizingMaskIntoConstraints == false) 48 | } 49 | 50 | @Test 51 | func testCollectionViewCellSubviews() { 52 | let cell = UICollectionViewCell() 53 | let v1 = UIView() 54 | let v2 = UIView() 55 | cell.subviews { 56 | v1 57 | v2 58 | } 59 | #expect(cell.contentView.subviews.count == 2) 60 | #expect(cell.contentView.subviews.contains(v1)) 61 | #expect(cell.contentView.subviews.contains(v2)) 62 | #expect(v1.translatesAutoresizingMaskIntoConstraints == false) 63 | #expect(v2.translatesAutoresizingMaskIntoConstraints == false) 64 | } 65 | 66 | @Test 67 | func testSubviewsCanBeNested() { 68 | let view = UIView() 69 | let v1 = UIView() 70 | let v2 = UIView() 71 | let v3 = UIView() 72 | let v4 = UIView() 73 | view.subviews { 74 | v1.subviews { 75 | v3 76 | v4 77 | } 78 | v2 79 | } 80 | #expect(view.subviews.count == 2) 81 | #expect(view.subviews.contains(v1)) 82 | #expect(view.subviews.contains(v2)) 83 | #expect(v1.subviews.count == 2) 84 | #expect(v1.subviews.contains(v3)) 85 | #expect(v1.subviews.contains(v4)) 86 | 87 | #expect(v1.translatesAutoresizingMaskIntoConstraints == false) 88 | #expect(v2.translatesAutoresizingMaskIntoConstraints == false) 89 | #expect(v3.translatesAutoresizingMaskIntoConstraints == false) 90 | #expect(v4.translatesAutoresizingMaskIntoConstraints == false) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/SteviaTests/PositionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CenterTests.swift 3 | // Stevia 4 | // 5 | // Created by Naabed on 13/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import Testing 10 | import UIKit 11 | import Stevia 12 | 13 | @Suite 14 | @MainActor class PositionTests { 15 | 16 | let win = UIWindow(frame: UIScreen.main.bounds) 17 | let ctrler = UIViewController() 18 | let v = UIView() 19 | 20 | init() { 21 | win.rootViewController = ctrler 22 | ctrler.view.subviews { v } 23 | v.size(100.0) 24 | } 25 | 26 | @Test 27 | func testTopDouble() { 28 | v.top(Double(23)) 29 | ctrler.view.layoutIfNeeded() 30 | #expect(v.frame.origin.y == 23) 31 | #expect(v.frame.origin.x == 0) 32 | #expect(v.frame.width == 100) 33 | #expect(v.frame.height == 100) 34 | } 35 | 36 | @Test 37 | func testTopCGFloat() { 38 | v.top(CGFloat(23)) 39 | ctrler.view.layoutIfNeeded() 40 | #expect(v.frame.origin.y == 23) 41 | #expect(v.frame.origin.x == 0) 42 | #expect(v.frame.width == 100) 43 | #expect(v.frame.height == 100) 44 | } 45 | 46 | @Test 47 | func testTopInt() { 48 | v.top(Int(23)) 49 | ctrler.view.layoutIfNeeded() 50 | #expect(v.frame.origin.y == 23) 51 | #expect(v.frame.origin.x == 0) 52 | #expect(v.frame.width == 100) 53 | #expect(v.frame.height == 100) 54 | } 55 | 56 | @Test 57 | func testBottomDouble() { 58 | v.bottom(Double(45)) 59 | ctrler.view.layoutIfNeeded() 60 | #expect(v.frame.origin.y == ctrler.view.frame.height - v.frame.height - 45) 61 | #expect(v.frame.origin.x == 0) 62 | #expect(v.frame.width == 100) 63 | #expect(v.frame.height == 100) 64 | } 65 | 66 | @Test 67 | func testBottomCGFloat() { 68 | v.bottom(CGFloat(45)) 69 | ctrler.view.layoutIfNeeded() 70 | #expect(v.frame.origin.y == ctrler.view.frame.height - v.frame.height - 45) 71 | #expect(v.frame.origin.x == 0) 72 | #expect(v.frame.width == 100) 73 | #expect(v.frame.height == 100) 74 | } 75 | 76 | @Test 77 | func testBottomInt() { 78 | v.bottom(Int(45)) 79 | ctrler.view.layoutIfNeeded() 80 | #expect(v.frame.origin.y == ctrler.view.frame.height - v.frame.height - 45) 81 | #expect(v.frame.origin.x == 0) 82 | #expect(v.frame.width == 100) 83 | #expect(v.frame.height == 100) 84 | } 85 | 86 | @Test 87 | func testLeftDouble() { 88 | v.left(Double(12)) 89 | ctrler.view.layoutIfNeeded() 90 | #expect(v.frame.origin.y == 0) 91 | #expect(v.frame.origin.x == 12) 92 | #expect(v.frame.width == 100) 93 | #expect(v.frame.height == 100) 94 | } 95 | 96 | @Test 97 | func testLeftCGFloat() { 98 | v.left(CGFloat(12)) 99 | ctrler.view.layoutIfNeeded() 100 | #expect(v.frame.origin.y == 0) 101 | #expect(v.frame.origin.x == 12) 102 | #expect(v.frame.width == 100) 103 | #expect(v.frame.height == 100) 104 | } 105 | 106 | @Test 107 | func testLeftInt() { 108 | v.left(Int(12)) 109 | ctrler.view.layoutIfNeeded() 110 | #expect(v.frame.origin.y == 0) 111 | #expect(v.frame.origin.x == 12) 112 | #expect(v.frame.width == 100) 113 | #expect(v.frame.height == 100) 114 | } 115 | 116 | @Test 117 | func testLeadingDouble() { 118 | v.leading(Double(12)) 119 | ctrler.view.layoutIfNeeded() 120 | #expect(v.frame.origin.y == 0) 121 | #expect(v.frame.origin.x == 12) 122 | #expect(v.frame.width == 100) 123 | #expect(v.frame.height == 100) 124 | } 125 | 126 | @Test 127 | func testLeadingCGFloat() { 128 | v.leading(CGFloat(12)) 129 | ctrler.view.layoutIfNeeded() 130 | #expect(v.frame.origin.y == 0) 131 | #expect(v.frame.origin.x == 12) 132 | #expect(v.frame.width == 100) 133 | #expect(v.frame.height == 100) 134 | } 135 | 136 | @Test 137 | func testLeadingInt() { 138 | v.leading(Int(12)) 139 | ctrler.view.layoutIfNeeded() 140 | #expect(v.frame.origin.y == 0) 141 | #expect(v.frame.origin.x == 12) 142 | #expect(v.frame.width == 100) 143 | #expect(v.frame.height == 100) 144 | } 145 | 146 | @Test 147 | func testRightDouble() { 148 | v.right(Double(74)) 149 | ctrler.view.layoutIfNeeded() 150 | #expect(v.frame.origin.y == 0) 151 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 74) 152 | #expect(v.frame.width == 100) 153 | #expect(v.frame.height == 100) 154 | } 155 | 156 | @Test 157 | func testRightCGFloat() { 158 | v.right(CGFloat(74)) 159 | ctrler.view.layoutIfNeeded() 160 | #expect(v.frame.origin.y == 0) 161 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 74) 162 | #expect(v.frame.width == 100) 163 | #expect(v.frame.height == 100) 164 | } 165 | 166 | @Test 167 | func testRightInt() { 168 | v.right(Int(74)) 169 | ctrler.view.layoutIfNeeded() 170 | #expect(v.frame.origin.y == 0) 171 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 74) 172 | #expect(v.frame.width == 100) 173 | #expect(v.frame.height == 100) 174 | } 175 | 176 | @Test 177 | func testTrailingDouble() { 178 | v.trailing(Double(74)) 179 | ctrler.view.layoutIfNeeded() 180 | #expect(v.frame.origin.y == 0) 181 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 74) 182 | #expect(v.frame.width == 100) 183 | #expect(v.frame.height == 100) 184 | } 185 | 186 | @Test 187 | func testTrailingCGFloat() { 188 | v.trailing(CGFloat(74)) 189 | ctrler.view.layoutIfNeeded() 190 | #expect(v.frame.origin.y == 0) 191 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 74) 192 | #expect(v.frame.width == 100) 193 | #expect(v.frame.height == 100) 194 | } 195 | 196 | @Test 197 | func testTrailingInt() { 198 | v.trailing(Int(74)) 199 | ctrler.view.layoutIfNeeded() 200 | #expect(v.frame.origin.y == 0) 201 | #expect(v.frame.origin.x == ctrler.view.frame.width - v.frame.width - 74) 202 | #expect(v.frame.width == 100) 203 | #expect(v.frame.height == 100) 204 | } 205 | 206 | @Test 207 | func testPercentTop() { 208 | v.top(10%) 209 | ctrler.view.layoutIfNeeded() 210 | #expect(isApproximatelyEqual(v.frame.origin.y, ctrler.view.frame.height * 0.1)) 211 | #expect(v.frame.origin.x == 0) 212 | #expect(v.frame.width == 100) 213 | #expect(v.frame.height == 100) 214 | } 215 | 216 | @Test 217 | func testPercentBottom() { 218 | v.bottom(10%) 219 | ctrler.view.layoutIfNeeded() 220 | #expect(isApproximatelyEqual(v.frame.origin.y, ctrler.view.frame.height * 0.9 - v.frame.height)) 221 | #expect(v.frame.origin.x == 0) 222 | #expect(v.frame.width == 100) 223 | #expect(v.frame.height == 100) 224 | } 225 | 226 | @Test 227 | func testPercentLeft() { 228 | v.left(10%) 229 | ctrler.view.layoutIfNeeded() 230 | #expect(v.frame.origin.y == 0) 231 | #expect(isApproximatelyEqual(v.frame.origin.x, ctrler.view.frame.width * 0.1)) 232 | #expect(v.frame.width == 100) 233 | #expect(v.frame.height == 100) 234 | } 235 | 236 | @Test 237 | func testPercentRight() { 238 | v.right(10%) 239 | ctrler.view.layoutIfNeeded() 240 | #expect(v.frame.origin.y == 0) 241 | #expect(isApproximatelyEqual(v.frame.origin.x, ctrler.view.frame.width * 0.9 - v.frame.width)) 242 | #expect(v.frame.width == 100) 243 | #expect(v.frame.height == 100) 244 | } 245 | 246 | @Test 247 | func testPercentLeading() { 248 | v.leading(10%) 249 | ctrler.view.layoutIfNeeded() 250 | #expect(v.frame.origin.y == 0) 251 | #expect(isApproximatelyEqual(v.frame.origin.x, ctrler.view.frame.width * 0.1)) 252 | #expect(v.frame.width == 100) 253 | #expect(v.frame.height == 100) 254 | } 255 | 256 | @Test 257 | func testPercentTrailing() { 258 | v.trailing(10%) 259 | ctrler.view.layoutIfNeeded() 260 | #expect(v.frame.origin.y == 0) 261 | #expect(isApproximatelyEqual(v.frame.origin.x, ctrler.view.frame.width * 0.9 - v.frame.width)) 262 | #expect(v.frame.width == 100) 263 | #expect(v.frame.height == 100) 264 | } 265 | 266 | @Test 267 | func testPercentLeadingRTL() { 268 | ctrler.view.semanticContentAttribute = .forceRightToLeft 269 | v.leading(10%) 270 | ctrler.view.layoutIfNeeded() 271 | #expect(v.frame.origin.y == 0) 272 | #expect(isApproximatelyEqual(v.frame.origin.x, (ctrler.view.frame.width * 0.9 - v.frame.width))) 273 | #expect(v.frame.width == 100) 274 | #expect(v.frame.height == 100) 275 | } 276 | 277 | @Test 278 | func testPercentTrailingRTL() { 279 | ctrler.view.semanticContentAttribute = .forceRightToLeft 280 | v.trailing(10%) 281 | ctrler.view.layoutIfNeeded() 282 | #expect(v.frame.origin.y == 0) 283 | #expect(isApproximatelyEqual(v.frame.origin.x, ctrler.view.frame.width * 0.1)) 284 | #expect(v.frame.width == 100) 285 | #expect(v.frame.height == 100) 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /Tests/SteviaTests/SizeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SizeTests.swift 3 | // Stevia 4 | // 5 | // Created by Naabed on 12/02/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import Testing 10 | import UIKit 11 | import Stevia 12 | 13 | @Suite 14 | @MainActor struct SizeTests { 15 | 16 | let win = UIWindow(frame: UIScreen.main.bounds) 17 | let ctrler = UIViewController() 18 | let v = UIView() 19 | 20 | init() { 21 | win.rootViewController = ctrler 22 | ctrler.view.subviews { 23 | v 24 | } 25 | } 26 | 27 | @Test 28 | func testAspectRatioDouble() { 29 | v.width(150).aspectRatio(Double(3.0/2.0)) 30 | ctrler.view.layoutIfNeeded() 31 | #expect(v.frame.origin.y == 0) 32 | #expect(v.frame.origin.x == 0) 33 | #expect(v.frame.width == 150) 34 | #expect(v.frame.height == 100) 35 | } 36 | 37 | @Test 38 | func testAspectRatioCGFloat() { 39 | v.width(150).aspectRatio(CGFloat(3.0/2.0)) 40 | ctrler.view.layoutIfNeeded() 41 | #expect(v.frame.origin.y == 0) 42 | #expect(v.frame.origin.x == 0) 43 | #expect(v.frame.width == 150) 44 | #expect(v.frame.height == 100) 45 | } 46 | 47 | @Test 48 | func testAspectRatioInt() { 49 | v.width(150).aspectRatio(Int(3)) 50 | ctrler.view.layoutIfNeeded() 51 | #expect(v.frame.origin.y == 0) 52 | #expect(v.frame.origin.x == 0) 53 | #expect(v.frame.width == 150) 54 | #expect(v.frame.height == 50) 55 | } 56 | 57 | @Test 58 | func testAspectRatioPercentage() { 59 | v.width(150).aspectRatio(150%) 60 | ctrler.view.layoutIfNeeded() 61 | #expect(v.frame.origin.y == 0) 62 | #expect(v.frame.origin.x == 0) 63 | #expect(v.frame.width == 150) 64 | #expect(v.frame.height == 100) 65 | } 66 | 67 | @Test 68 | func testSizeDouble() { 69 | v.size(Double(57)) 70 | ctrler.view.layoutIfNeeded() 71 | #expect(v.frame.origin.y == 0) 72 | #expect(v.frame.origin.x == 0) 73 | #expect(v.frame.width == 57) 74 | #expect(v.frame.height == 57) 75 | } 76 | 77 | @Test 78 | func testSizeCGFloat() { 79 | v.size(CGFloat(57)) 80 | ctrler.view.layoutIfNeeded() 81 | #expect(v.frame.origin.y == 0) 82 | #expect(v.frame.origin.x == 0) 83 | #expect(v.frame.width == 57) 84 | #expect(v.frame.height == 57) 85 | } 86 | 87 | @Test 88 | func testSizeInt() { 89 | v.size(Int(57)) 90 | ctrler.view.layoutIfNeeded() 91 | #expect(v.frame.origin.y == 0) 92 | #expect(v.frame.origin.x == 0) 93 | #expect(v.frame.width == 57) 94 | #expect(v.frame.height == 57) 95 | } 96 | 97 | @Test 98 | func testWidthAndHeightDouble() { 99 | v.width(Double(36)) 100 | v.height(Double(23)) 101 | ctrler.view.layoutIfNeeded() 102 | #expect(v.frame.origin.y == 0) 103 | #expect(v.frame.origin.x == 0) 104 | #expect(v.frame.width == 36) 105 | #expect(v.frame.height == 23) 106 | } 107 | 108 | @Test 109 | func testWidthAndHeightCGFloat() { 110 | v.width(CGFloat(36)) 111 | v.height(CGFloat(23)) 112 | ctrler.view.layoutIfNeeded() 113 | #expect(v.frame.origin.y == 0) 114 | #expect(v.frame.origin.x == 0) 115 | #expect(v.frame.width == 36) 116 | #expect(v.frame.height == 23) 117 | } 118 | 119 | @Test 120 | func testWidthAndHeightInt() { 121 | v.width(Int(36)) 122 | v.height(Int(23)) 123 | ctrler.view.layoutIfNeeded() 124 | #expect(v.frame.origin.y == 0) 125 | #expect(v.frame.origin.x == 0) 126 | #expect(v.frame.width == 36) 127 | #expect(v.frame.height == 23) 128 | } 129 | 130 | @Test 131 | func testHeightPercentage() { 132 | v.width(100) 133 | v.height(40%) 134 | ctrler.view.layoutIfNeeded() 135 | #expect(v.frame.origin.y == 0) 136 | #expect(v.frame.origin.x == 0) 137 | #expect(v.frame.width == 100) 138 | #expect(isApproximatelyEqual(v.frame.height, ctrler.view.frame.height*0.4)) 139 | } 140 | 141 | @Test 142 | func testWidthPercentage() { 143 | v.height(100) 144 | v.width(87%) 145 | ctrler.view.layoutIfNeeded() 146 | #expect(v.frame.origin.y == 0) 147 | #expect(v.frame.origin.x == 0) 148 | #expect(v.frame.height == 100) 149 | #expect(isApproximatelyEqual(v.frame.width, ctrler.view.frame.width*0.87)) 150 | } 151 | 152 | @Test 153 | func testEqualSizes() { 154 | let width = 24.0 155 | let height = 267.0 156 | let v1 = UIView() 157 | let v2 = UIView() 158 | ctrler.view.subviews { 159 | v1 160 | v2 161 | } 162 | v1.height(height) 163 | v1.width(width) 164 | equal(sizes: [v1, v2]) 165 | ctrler.view.layoutIfNeeded() 166 | #expect(v1.frame.width == v2.frame.width) 167 | #expect(v1.frame.height == v2.frame.height) 168 | } 169 | 170 | @Test 171 | func testVariadicEqualSizes() { 172 | let width = 24.0 173 | let height = 267.0 174 | let v1 = UIView() 175 | let v2 = UIView() 176 | ctrler.view.subviews { 177 | v1 178 | v2 179 | } 180 | v1.height(height) 181 | v1.width(width) 182 | equal(sizes: v1, v2) 183 | ctrler.view.layoutIfNeeded() 184 | #expect(v1.frame.width == v2.frame.width) 185 | #expect(v1.frame.height == v2.frame.height) 186 | } 187 | 188 | @Test 189 | func testFollwEdges() { 190 | let v1 = UIView() 191 | let v2 = UIView() 192 | ctrler.view.subviews { 193 | v1 194 | v2 195 | } 196 | 197 | let test = ctrler.view 198 | 199 | test!.layout { 200 | 10 201 | |-20-v1| ~ 32 202 | } 203 | 204 | ctrler.view.layoutIfNeeded() 205 | 206 | #expect(v1.frame.origin.y == 10) 207 | #expect(v1.frame.origin.x == 20) 208 | #expect(v1.frame.width == ctrler.view.frame.width - 20) 209 | #expect(v1.frame.height == 32) 210 | 211 | #expect(v2.frame.origin.y == 0) 212 | #expect(v2.frame.origin.x == 0) 213 | #expect(v2.frame.width == 0) 214 | #expect(v2.frame.height == 0) 215 | 216 | v2.followEdges(v1) 217 | ctrler.view.layoutIfNeeded() 218 | #expect(v2.frame.origin.y == v1.frame.origin.y) 219 | #expect(v2.frame.origin.x == v1.frame.origin.x) 220 | #expect(v2.frame.width == v1.frame.width) 221 | #expect(v2.frame.height == v1.frame.height) 222 | } 223 | 224 | @Test 225 | func testHeightEqualWidth() { 226 | v.heightEqualsWidth().width(85) 227 | ctrler.view.layoutIfNeeded() 228 | #expect(v.frame.origin.y == 0) 229 | #expect(v.frame.origin.x == 0) 230 | #expect(v.frame.width == 85) 231 | #expect(v.frame.height == 85) 232 | } 233 | 234 | @Test 235 | func testWidthEqualHeight() { 236 | v.height(192) 237 | v.heightEqualsWidth() 238 | ctrler.view.layoutIfNeeded() 239 | #expect(v.frame.origin.y == 0) 240 | #expect(v.frame.origin.x == 0) 241 | #expect(v.frame.width == 192) 242 | #expect(v.frame.height == 192) 243 | } 244 | 245 | @Test 246 | func testSizeOnOrphanView() { 247 | v.removeFromSuperview() 248 | v.height(80) 249 | v.width(80) 250 | ctrler.view?.subviews { v } 251 | 252 | let view: UIView = ctrler.view 253 | view.layout { 254 | 0 255 | |v 256 | } 257 | 258 | ctrler.view.layoutIfNeeded() 259 | #expect(v.frame.width == 80) 260 | #expect(v.frame.height == 80) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Tests/SteviaTests/StyleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyleTests.swift 3 | // Stevia 4 | // 5 | // Created by krzat on 17/03/16. 6 | // Copyright © 2016 Sacha Durand Saint Omer. All rights reserved. 7 | // 8 | 9 | import Testing 10 | import UIKit 11 | import Stevia 12 | 13 | @Suite 14 | @MainActor struct StyleTests { 15 | 16 | func styleView(_ view: UIView) { 17 | view.backgroundColor = UIColor.yellow 18 | } 19 | 20 | func styleLabel(_ label: UILabel) { 21 | label.textColor = UIColor.yellow 22 | } 23 | 24 | @Test 25 | func canCallStyle() { 26 | let label = UILabel() 27 | 28 | label.style(styleLabel).style(styleView) 29 | label.style(styleView).style(styleLabel) 30 | 31 | let view: UIView = label 32 | view.style(styleView) 33 | 34 | #expect(view.backgroundColor == UIColor.yellow) 35 | #expect(label.textColor == UIColor.yellow) 36 | 37 | //check type deduction 38 | label.style { (label) -> () in 39 | label.textColor = UIColor.blue 40 | } 41 | #expect(label.textColor == UIColor.blue) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freshOS/Stevia/7eb652374263868ff61f6ba15e0eb3b830289485/banner.png --------------------------------------------------------------------------------