├── Sources └── ViewKit │ ├── Internal │ ├── @_exported.swift │ ├── Notification.swift │ └── ClosureTapGestureRecognizer.swift │ ├── Components │ ├── For.swift │ ├── Filler.swift │ ├── ProgrammaticView.swift │ ├── VerticalStack.swift │ ├── HorizontalStack.swift │ └── ViewController.swift │ ├── Builders │ ├── StackViewBuilder.swift │ ├── ProgrammaticViewBuilder.swift │ └── ConstraintBuilder.swift │ ├── Extensions │ ├── UIScene+extensions.swift │ ├── UIControl+extensions.swift │ ├── UIStackView+extensions.swift │ ├── UIImageView+extensions.swift │ ├── UILabel+extensions.swift │ ├── UIButton+extensions.swift │ ├── UITextField+extensions.swift │ └── UIView+extensions.swift │ └── Preview │ └── UIViewPreview.swift ├── Sample ├── ViewKitSample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── SceneDelegate.swift │ ├── Info.plist │ ├── SampleView.swift │ └── Base.lproj │ │ └── LaunchScreen.storyboard └── ViewKitSample.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── project.pbxproj ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Package.swift ├── LICENSE ├── .gitignore ├── README.md └── Tests └── ViewKitTests └── UILabel+tests.swift /Sources/ViewKit/Internal/@_exported.swift: -------------------------------------------------------------------------------- 1 | @_exported import UIKit 2 | -------------------------------------------------------------------------------- /Sample/ViewKitSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main final class AppDelegate: UIResponder, UIApplicationDelegate {} 4 | -------------------------------------------------------------------------------- /Sample/ViewKitSample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/ViewKit/Components/For.swift: -------------------------------------------------------------------------------- 1 | public func For( 2 | _ items: T, 3 | _ builder: (T.Element) -> Content 4 | ) -> [Content] { 5 | return items.map(builder) 6 | } 7 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sample/ViewKitSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sample/ViewKitSample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ViewKit/Builders/StackViewBuilder.swift: -------------------------------------------------------------------------------- 1 | @resultBuilder 2 | public struct StackViewBuilder { 3 | public static func buildBlock(_ children: UIView...) -> [UIView] { children } 4 | public static func buildBlock(_ children: [UIView]) -> [UIView] { children } 5 | } 6 | -------------------------------------------------------------------------------- /Sources/ViewKit/Components/Filler.swift: -------------------------------------------------------------------------------- 1 | open class Filler: UIView { 2 | public init() { 3 | super.init(frame: .zero) 4 | maxSize() 5 | } 6 | 7 | public required init?(coder: NSCoder) { 8 | fatalError("init(coder:) has not been implemented") 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sample/ViewKitSample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sample/ViewKitSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sample/ViewKitSample/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import ViewKit 2 | 3 | final class SceneDelegate: UIResponder, UIWindowSceneDelegate { 4 | var window: UIWindow? 5 | 6 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 7 | window = scene.window(with: SampleView().embeddedInViewController()) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/ViewKit/Components/ProgrammaticView.swift: -------------------------------------------------------------------------------- 1 | public protocol ProgrammaticView { 2 | @ProgrammaticViewBuilder var body: UIView { get } 3 | } 4 | 5 | extension UIView: ProgrammaticView { 6 | public var body: UIView { 7 | self 8 | } 9 | } 10 | 11 | public extension ProgrammaticView { 12 | func embeddedInViewController() -> UIViewController { 13 | return ViewController(with: self) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/ViewKit/Extensions/UIScene+extensions.swift: -------------------------------------------------------------------------------- 1 | @available(iOS 13, *) 2 | public extension UIScene { 3 | func window(with rootViewController: UIViewController) -> UIWindow? { 4 | guard let windowScene = (self as? UIWindowScene) else { return nil } 5 | let window = UIWindow(windowScene: windowScene) 6 | window.rootViewController = rootViewController 7 | window.makeKeyAndVisible() 8 | return window 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/ViewKit/Components/VerticalStack.swift: -------------------------------------------------------------------------------- 1 | public class VerticalStack: UIStackView { 2 | public convenience init( 3 | spacing: CGFloat = 10, 4 | distribution: Distribution = .fill, 5 | alignment: Alignment = .center, 6 | @StackViewBuilder _ builder: () -> [UIView] 7 | ) { 8 | self.init(arrangedSubviews: builder()) 9 | self.axis = .vertical 10 | self.spacing = spacing 11 | self.distribution = distribution 12 | self.alignment = alignment 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ViewKit/Components/HorizontalStack.swift: -------------------------------------------------------------------------------- 1 | public class HorizontalStack: UIStackView { 2 | public convenience init( 3 | spacing: CGFloat = 10, 4 | distribution: Distribution = .fillProportionally, 5 | alignment: Alignment = .center, 6 | @StackViewBuilder _ builder: () -> [UIView] 7 | ) { 8 | self.init(arrangedSubviews: builder()) 9 | self.axis = .horizontal 10 | self.spacing = spacing 11 | self.distribution = distribution 12 | self.alignment = alignment 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ViewKit", 7 | platforms: [ 8 | .iOS(.v11) 9 | ], 10 | products: [ 11 | .library( 12 | name: "ViewKit", 13 | targets: ["ViewKit"]), 14 | ], 15 | dependencies: [ 16 | ], 17 | targets: [ 18 | .target( 19 | name: "ViewKit", 20 | dependencies: []), 21 | .testTarget( 22 | name: "ViewKitTests", 23 | dependencies: ["ViewKit"]), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /Sources/ViewKit/Preview/UIViewPreview.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @available(iOS 13.0, *) 4 | public struct PreviewContainer: UIViewRepresentable { 5 | private let view: View 6 | 7 | public init(_ builder: @escaping () -> View) { 8 | view = builder() 9 | } 10 | 11 | public func makeUIView(context: Context) -> UIView { 12 | return view.body 13 | } 14 | 15 | public func updateUIView(_ view: UIView, context: Context) { 16 | view.setContentHuggingPriority(.defaultHigh, for: .horizontal) 17 | view.setContentHuggingPriority(.defaultHigh, for: .vertical) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ViewKit/Internal/Notification.swift: -------------------------------------------------------------------------------- 1 | extension Notification { 2 | static let ProgrammaticViewContentUpdated = Notification(name: .ProgrammaticViewContentUpdated) 3 | } 4 | 5 | extension Notification.Name { 6 | static let ProgrammaticViewContentUpdated = Notification.Name("ProgrammaticViewContentUpdated") 7 | } 8 | 9 | func when(_ notificationName: Notification.Name, do action: @escaping () -> Void) { 10 | NotificationCenter.default.addObserver(forName: notificationName, object: nil, queue: nil) { _ in 11 | action() 12 | } 13 | } 14 | 15 | func notify(_ notificationNamed: Notification.Name) { 16 | NotificationCenter.default.post(.ProgrammaticViewContentUpdated) 17 | } 18 | -------------------------------------------------------------------------------- /Sample/ViewKitSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Sources/ViewKit/Extensions/UIControl+extensions.swift: -------------------------------------------------------------------------------- 1 | public extension UIControl { 2 | 3 | // MARK: - Managing state 4 | 5 | @discardableResult 6 | func isEnabled(_ isEnabled: Bool = true) -> Self { 7 | self.isEnabled = isEnabled 8 | return self 9 | } 10 | 11 | @discardableResult 12 | func isSelected(_ isSelected: Bool = true) -> Self { 13 | self.isSelected = isSelected 14 | return self 15 | } 16 | 17 | 18 | @discardableResult 19 | func isHighlighted(_ isHighlighted: Bool = true) -> Self { 20 | self.isHighlighted = isHighlighted 21 | return self 22 | } 23 | 24 | @discardableResult 25 | func contentVerticalAlignment(_ contentVerticalAlignment: ContentVerticalAlignment) -> Self { 26 | self.contentVerticalAlignment = contentVerticalAlignment 27 | return self 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lucas Werner Kuipers 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 | -------------------------------------------------------------------------------- /Sources/ViewKit/Builders/ProgrammaticViewBuilder.swift: -------------------------------------------------------------------------------- 1 | @resultBuilder 2 | public struct ProgrammaticViewBuilder { 3 | public static func buildBlock(_ child: UIView) -> UIView { child } 4 | public static func buildBlock(_ children: UIView...) -> UIView { 5 | let container = UIView() 6 | 7 | children.forEach { child in 8 | container.addSubview(child) 9 | 10 | child.prepareForConstraints() 11 | 12 | container.addingSubview(child) 13 | 14 | container.constrain([ 15 | child.topAnchor.constraint(greaterThanOrEqualTo: container.topAnchor), 16 | child.leftAnchor.constraint(greaterThanOrEqualTo: container.leftAnchor), 17 | child.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor), 18 | child.rightAnchor.constraint(lessThanOrEqualTo: container.rightAnchor), 19 | child.centerXAnchor.constraint(equalTo: container.centerXAnchor), 20 | child.centerYAnchor.constraint(equalTo: container.centerYAnchor), 21 | ]) 22 | } 23 | 24 | return container 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/ViewKit/Extensions/UIStackView+extensions.swift: -------------------------------------------------------------------------------- 1 | public extension UIStackView { 2 | func spacing(_ spacing: CGFloat) -> Self { 3 | self.spacing = spacing 4 | return self 5 | } 6 | 7 | func alignment(_ alignment: UIStackView.Alignment) -> Self { 8 | self.alignment = alignment 9 | return self 10 | } 11 | 12 | func distribution(_ distribution: UIStackView.Distribution) -> Self { 13 | self.distribution = distribution 14 | return self 15 | } 16 | 17 | func axis(_ axis: NSLayoutConstraint.Axis) -> Self { 18 | self.axis = axis 19 | return self 20 | } 21 | 22 | func isBaselineRelativeArrangement(_ isBaselineRelativeArrangement: Bool) -> Self { 23 | self.isBaselineRelativeArrangement = isBaselineRelativeArrangement 24 | return self 25 | } 26 | 27 | func isLayoutMarginsRelativeArrangement(_ isLayoutMarginsRelativeArrangement: Bool) -> Self { 28 | self.isLayoutMarginsRelativeArrangement = isLayoutMarginsRelativeArrangement 29 | return self 30 | } 31 | 32 | func addArrangedSubviews(@StackViewBuilder _ builder: () -> [UIView]) { 33 | for view in builder() { 34 | addArrangedSubview(view) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sample/ViewKitSample/SampleView.swift: -------------------------------------------------------------------------------- 1 | import ViewKit 2 | 3 | final class SampleView: ProgrammaticView { 4 | var body: UIView { 5 | VerticalStack { 6 | UIImageView(systemName: "person.circle.fill") 7 | .contentMode(.scaleAspectFit) 8 | .size(160) 9 | .padding(50) 10 | .accessibilityIdentifier("Person image") 11 | .onTap(numberOfTapsRequired: 2) { view in 12 | print("\(view.accessibilityIdentifier.debugDescription) tapped twice") 13 | } 14 | .onSwipe(direction: .up) { view in 15 | print("\(view.accessibilityIdentifier.debugDescription) swiped up") 16 | } 17 | .onLongPress { view in 18 | print("\(view.accessibilityIdentifier.debugDescription) long press") 19 | } 20 | .onPan { view in 21 | print("\(view.accessibilityIdentifier.debugDescription) pan") 22 | } 23 | .onPinch { view in 24 | print("\(view.accessibilityIdentifier.debugDescription) pinched") 25 | } 26 | .onRotation { view in 27 | print("\(view.accessibilityIdentifier.debugDescription) rotation") 28 | } 29 | 30 | UITextField() 31 | .tag(0) 32 | .placeholder("Username") 33 | .autocapitalizationType(.none) 34 | .autocorrectionType(.no) 35 | .spellCheckingType(.no) 36 | .contentHuggingPriority(.required, for: .vertical) 37 | .padding(15) 38 | .round() 39 | .maxWidth() 40 | } 41 | } 42 | } 43 | 44 | // MARK: - Previews 45 | 46 | import SwiftUI 47 | 48 | struct SampleView_Previews: PreviewProvider { 49 | static var previews: some View { 50 | PreviewContainer { 51 | SampleView() 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ViewKit/Internal/ClosureTapGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | final class ClosureTapGestureRecognizer: UITapGestureRecognizer { 2 | private var actionClosure: (() -> Void)? 3 | 4 | func addAction(_ closure: @escaping () -> Void) { 5 | actionClosure = closure 6 | addTarget(self, action: #selector(handleTap)) 7 | } 8 | 9 | @objc private func handleTap() { 10 | actionClosure?() 11 | } 12 | } 13 | 14 | final class ClosureLongPressGestureRecognizer: UILongPressGestureRecognizer { 15 | private var actionClosure: (() -> Void)? 16 | 17 | func addAction(_ closure: @escaping () -> Void) { 18 | actionClosure = closure 19 | addTarget(self, action: #selector(handleLongPress)) 20 | } 21 | 22 | @objc private func handleLongPress() { 23 | actionClosure?() 24 | } 25 | } 26 | 27 | final class ClosureSwipeGestureRecognizer: UISwipeGestureRecognizer { 28 | private var actionClosure: (() -> Void)? 29 | 30 | func addAction(_ closure: @escaping () -> Void) { 31 | actionClosure = closure 32 | addTarget(self, action: #selector(handleSwipe)) 33 | } 34 | 35 | @objc private func handleSwipe() { 36 | actionClosure?() 37 | } 38 | } 39 | 40 | final class ClosurePinchGestureRecognizer: UIPinchGestureRecognizer { 41 | private var actionClosure: (() -> Void)? 42 | 43 | func addAction(_ closure: @escaping () -> Void) { 44 | actionClosure = closure 45 | addTarget(self, action: #selector(handlePinch)) 46 | } 47 | 48 | @objc private func handlePinch() { 49 | actionClosure?() 50 | } 51 | } 52 | 53 | final class ClosureRotationGestureRecognizer: UIRotationGestureRecognizer { 54 | private var actionClosure: (() -> Void)? 55 | 56 | func addAction(_ closure: @escaping () -> Void) { 57 | actionClosure = closure 58 | addTarget(self, action: #selector(handleRotation)) 59 | } 60 | 61 | @objc private func handleRotation() { 62 | actionClosure?() 63 | } 64 | } 65 | 66 | final class ClosurePanGestureRecognizer: UIPanGestureRecognizer { 67 | private var actionClosure: (() -> Void)? 68 | 69 | func addAction(_ closure: @escaping () -> Void) { 70 | actionClosure = closure 71 | addTarget(self, action: #selector(handlePan)) 72 | } 73 | 74 | @objc private func handlePan() { 75 | actionClosure?() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /Sources/ViewKit/Extensions/UIImageView+extensions.swift: -------------------------------------------------------------------------------- 1 | public extension UIImageView { 2 | 3 | @available(iOS 13.0, *) 4 | convenience init(systemName: String) { 5 | self.init(image: UIImage(systemName: systemName)) 6 | } 7 | 8 | convenience init(_ name: String) { 9 | self.init(image: UIImage(named: name)) 10 | } 11 | 12 | // MARK: - Acessing the displayed images 13 | 14 | @discardableResult 15 | func image(_ image: UIImage?) -> Self { 16 | self.image = image 17 | return self 18 | } 19 | 20 | @discardableResult 21 | func highlightedImage(_ highlightedImage: UIImage?) -> Self { 22 | self.highlightedImage = highlightedImage 23 | return self 24 | } 25 | 26 | // MARK: - Animating a sequence of images 27 | 28 | @discardableResult 29 | func animationImages(_ animationImages: [UIImage]?) -> Self { 30 | self.animationImages = animationImages 31 | return self 32 | } 33 | 34 | @discardableResult 35 | func highlightedAnimationImages(_ highlightedAnimationImages: [UIImage]?) -> Self { 36 | self.highlightedAnimationImages = highlightedAnimationImages 37 | return self 38 | } 39 | 40 | @discardableResult 41 | func animationDuration(_ animationDuration: TimeInterval) -> Self { 42 | self.animationDuration = animationDuration 43 | return self 44 | } 45 | 46 | @discardableResult 47 | func animationRepeatCount(_ animationRepeatCount: Int) -> Self { 48 | self.animationRepeatCount = animationRepeatCount 49 | return self 50 | } 51 | 52 | @discardableResult 53 | func startAnimating(_ start: Bool = true) -> Self { 54 | start ? startAnimating() : nil 55 | return self 56 | } 57 | 58 | @discardableResult 59 | func stopAnimating(_ stop: Bool = true) -> Self { 60 | stop ? stopAnimating() : nil 61 | return self 62 | } 63 | 64 | // MARK: - Configuring the appearance of symbol images 65 | 66 | @available(iOS 13.0, *) 67 | @discardableResult 68 | func preferredSymbolConfiguration(_ preferredSymbolConfiguration: UIImage.SymbolConfiguration) -> Self { 69 | self.preferredSymbolConfiguration = preferredSymbolConfiguration 70 | return self 71 | } 72 | 73 | // MARK: - Configuring the image view 74 | 75 | @discardableResult 76 | func isHighlighted(_ isHighlighted: Bool) -> Self { 77 | self.isHighlighted = isHighlighted 78 | return self 79 | } 80 | 81 | @discardableResult 82 | func tintColor(_ tintColor: UIColor) -> Self { 83 | self.tintColor = tintColor 84 | return self 85 | } 86 | 87 | @discardableResult 88 | func contentMode(_ mode: UIImageView.ContentMode) -> Self { 89 | self.contentMode = mode 90 | return self 91 | } 92 | 93 | @discardableResult 94 | func renderingMode(_ mode: UIImage.RenderingMode = .automatic) -> Self { 95 | self.image = image?.withRenderingMode(mode) 96 | return self 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sample/ViewKitSample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ViewKit 2 | 3 | ViewKit is a lightweight and user-friendly Swift package that brings SwiftUI-like, declarative syntax to UIKit. It provides a way to write UIKit code in a declarative and chainable manner, similar to SwiftUI, making UIKit development more streamlined and approachable. Most customization properties available in UIKit are supported in ViewKit using method chaining for a more fluid and readable syntax. 4 | 5 | ![ViewKit](https://user-images.githubusercontent.com/59176579/222473974-87773492-ef28-4e1e-b0fd-08c953bdd3e3.png) 6 | 7 | ## Installation 8 | 9 | You can install ViewKit using Swift Package Manager. Simply add the following line to your `Package.swift` file: 10 | 11 | ```swift 12 | dependencies: [ 13 | .package(url: "https://github.com/lucaswkuipers/ViewKit.git", from: "1.0.0") 14 | ] 15 | ``` 16 | 17 | ## Usage 18 | 19 | ### ProgrammaticView 20 | 21 | `ProgrammaticView` is the main protocol in ViewKit for creating views. Here's how you can use it: 22 | 23 | Define your view by implementing the `ProgrammaticView` protocol and providing your layout within the `body` property. 24 | 25 | Here's an example: 26 | 27 | ```swift 28 | import ViewKit 29 | 30 | final class MyView: ProgrammaticView { 31 | 32 | var body: UIView { 33 | VerticalStack(spacing: 20) { 34 | UILabel("Hello, world!") 35 | .font(.preferredFont(forTextStyle: .largeTitle)) 36 | 37 | UIButton("Tap me!") { _ in print("Tapped") } 38 | .font(.preferredFont(forTextStyle: .title1)) 39 | .backgroundColor(.systemBlue) 40 | .cornerRadius(10) 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | #### VerticalStack and HorizontalStack 47 | 48 | `VerticalStack` and `HorizontalStack` work similarly to `VStack` and `HStack` in SwiftUI. They are used to stack views vertically and horizontally, respectively. 49 | 50 | ```swift 51 | VerticalStack(spacing: 20) { 52 | // Your views here 53 | } 54 | 55 | HorizontalStack(spacing: 20) { 56 | // Your views here 57 | } 58 | ``` 59 | 60 | Additionally, `UIView` can be used similarly to `ZStack` in SwiftUI, for stacking views on top of each other. 61 | 62 | #### Filler 63 | 64 | `Filler` works similar to `Spacer` in SwiftUI. It pushes adjacent views away, taking up all available space. 65 | 66 | ```swift 67 | HorizontalStack(spacing: 20) { 68 | UILabel("Hello") 69 | Filler() 70 | UILabel("World") 71 | } 72 | ``` 73 | 74 | In this example, the "Hello" and "World" labels will be pushed to opposite sides of the screen, with all the remaining space in between being filled by the `Filler`. 75 | 76 | ### ViewController 77 | 78 | You can use this `ProgrammaticView` with a custom `ViewController` that takes a `ProgrammaticView` in its constructor and sets it as its view. 79 | 80 | Here's an example: 81 | 82 | ```swift 83 | let viewController = ViewController(with: MyView()) 84 | ``` 85 | 86 | ### Previews 87 | 88 | You can also preview your ViewKit views in the Xcode canvas by embedding your `ProgrammaticView` in a `PreviewContainer` like so: 89 | 90 | ```swift 91 | import SwiftUI 92 | 93 | struct MyView_Previews: PreviewProvider { 94 | static var previews: some View { 95 | PreviewContainer { 96 | MyView() 97 | } 98 | } 99 | } 100 | ``` 101 | 102 | ## Concepts 103 | 104 | ### Result Builders and Fluent Interfaces 105 | 106 | ViewKit leverages Swift's result builders and fluent interfaces to create a more declarative syntax and reduce boilerplate code. This helps keep your UIKit code clean and easy to understand. 107 | 108 | ## License 109 | 110 | ViewKit is available under the MIT license. See the LICENSE file for more information. 111 | 112 | ## Swift Package Index 113 | 114 | Check out on Swift Package Index: [https://swiftpackageindex.com/lucaswkuipers/ViewKit](https://swiftpackageindex.com/lucaswkuipers/ViewKit) 115 | -------------------------------------------------------------------------------- /Sources/ViewKit/Extensions/UILabel+extensions.swift: -------------------------------------------------------------------------------- 1 | public extension UILabel { 2 | 3 | convenience init(_ text: String) { 4 | self.init(frame: .zero) 5 | self.text = text 6 | } 7 | 8 | // MARK: - Text Attributes 9 | 10 | @discardableResult 11 | func text(_ text: String?) -> Self { 12 | self.text = text 13 | return self 14 | } 15 | 16 | @discardableResult 17 | func attributedText(_ attributedText: NSAttributedString?) -> Self { 18 | self.attributedText = attributedText 19 | return self 20 | } 21 | 22 | @discardableResult 23 | func font(_ font: UIFont?) -> Self { 24 | self.font = font 25 | return self 26 | } 27 | 28 | @discardableResult 29 | func textColor(_ textColor: UIColor?) -> Self { 30 | self.textColor = textColor 31 | return self 32 | } 33 | 34 | @discardableResult 35 | func textAlignment(_ textAlignment: NSTextAlignment) -> Self { 36 | self.textAlignment = textAlignment 37 | return self 38 | } 39 | 40 | @discardableResult 41 | func lineBreakMode(_ lineBreakMode: NSLineBreakMode) -> Self { 42 | self.lineBreakMode = lineBreakMode 43 | return self 44 | } 45 | 46 | @available(iOS 14.0, *) 47 | @discardableResult 48 | func lineBreakStrategy(_ lineBreakStrategy: NSParagraphStyle.LineBreakStrategy) -> Self { 49 | self.lineBreakStrategy = lineBreakStrategy 50 | return self 51 | } 52 | 53 | @discardableResult 54 | func isEnabled(_ isEnabled: Bool) -> Self { 55 | self.isEnabled = isEnabled 56 | return self 57 | } 58 | 59 | @discardableResult 60 | func showsExpansionTextWhenTruncated(_ showsExpansionTextWhenTruncated: Bool) -> Self { 61 | self.showsExpansionTextWhenTruncated = showsExpansionTextWhenTruncated 62 | return self 63 | } 64 | 65 | // MARK: - Sizing text 66 | 67 | @discardableResult 68 | func adjustsFontSizeToFitWidth(_ adjustsFontSizeToFitWidth: Bool = true) -> Self { 69 | self.adjustsFontSizeToFitWidth = adjustsFontSizeToFitWidth 70 | return self 71 | } 72 | 73 | @discardableResult 74 | func allowsDefaultTighteningForTruncation(_ allowsDefaultTighteningForTruncation: Bool = true) -> Self { 75 | self.allowsDefaultTighteningForTruncation = allowsDefaultTighteningForTruncation 76 | return self 77 | } 78 | 79 | @discardableResult 80 | func baselineAdjustment(_ baselineAdjustment: UIBaselineAdjustment) -> Self { 81 | self.baselineAdjustment = baselineAdjustment 82 | return self 83 | } 84 | 85 | @discardableResult 86 | func minimumScaleFactor(_ minimumScaleFactor: CGFloat) -> Self { 87 | self.minimumScaleFactor = minimumScaleFactor 88 | return self 89 | } 90 | 91 | @discardableResult 92 | func numberOfLines(_ numberOfLines: Int) -> Self { 93 | self.numberOfLines = numberOfLines 94 | return self 95 | } 96 | 97 | // MARK: - Highlight 98 | 99 | @discardableResult 100 | func highlightedTextColor(_ highlightedTextColor: UIColor?) -> Self { 101 | self.highlightedTextColor = highlightedTextColor 102 | return self 103 | } 104 | 105 | @discardableResult 106 | func isHighlighted(_ isHighlighted: Bool) -> Self { 107 | self.isHighlighted = isHighlighted 108 | return self 109 | } 110 | 111 | // MARK: - Shadow 112 | 113 | @discardableResult 114 | func shadowColor(_ shadowColor: UIColor?) -> Self { 115 | self.shadowColor = shadowColor 116 | return self 117 | } 118 | 119 | @discardableResult 120 | func shadowOffset(_ shadowOffset: CGSize) -> Self { 121 | self.shadowOffset = shadowOffset 122 | return self 123 | } 124 | 125 | @discardableResult 126 | func preferredMaxLayoutWidth(_ preferredMaxLayoutWidth: CGFloat) -> Self { 127 | self.preferredMaxLayoutWidth = preferredMaxLayoutWidth 128 | return self 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Tests/ViewKitTests/UILabel+tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import ViewKit 3 | 4 | final class UILabelTests: XCTestCase { 5 | func test_text_modiefiesLabelCorrectly() { 6 | let sut = UILabel() 7 | 8 | sut.text("text") 9 | 10 | XCTAssertEqual(sut.text, "text") 11 | } 12 | 13 | func test_attributedText_modifiesLabelCorrectly() { 14 | let sut = UILabel() 15 | 16 | sut.attributedText(.init(string: "attr")) 17 | 18 | XCTAssertEqual(sut.attributedText, .init(string: "attr")) 19 | } 20 | 21 | func test_font_modiefiesLabelCorrectly() { 22 | let sut = UILabel() 23 | 24 | sut.font(.systemFont(ofSize: 12)) 25 | 26 | XCTAssertEqual(sut.font, .systemFont(ofSize: 12)) 27 | } 28 | func test_textColor_modiefiesLabelCorrectly() { 29 | let sut = UILabel() 30 | 31 | sut.textColor(.black) 32 | 33 | XCTAssertEqual(sut.textColor, .black) 34 | } 35 | func test_textAlignment_modiefiesLabelCorrectly() { 36 | let sut = UILabel() 37 | 38 | sut.textAlignment(.center) 39 | 40 | XCTAssertEqual(sut.textAlignment, .center) 41 | } 42 | 43 | func test_lineBreakMode_modiefiesLabelCorrectly() { 44 | let sut = UILabel() 45 | 46 | sut.lineBreakMode(.byCharWrapping) 47 | 48 | XCTAssertEqual(sut.lineBreakMode, .byCharWrapping) 49 | } 50 | 51 | func test_lineBreakStrategy_modiefiesLabelCorrectly() { 52 | let sut = UILabel() 53 | 54 | if #available(iOS 14.0, *) { 55 | sut.lineBreakStrategy(.hangulWordPriority) 56 | } else { 57 | // Fallback on earlier versions 58 | } 59 | 60 | if #available(iOS 14.0, *) { 61 | XCTAssertEqual(sut.lineBreakStrategy, .hangulWordPriority) 62 | } else { 63 | // Fallback on earlier versions 64 | } 65 | } 66 | func test_showsExpansionTextWhenTruncated_modiefiesLabelCorrectly() { 67 | let sut = UILabel() 68 | 69 | sut.showsExpansionTextWhenTruncated(true) 70 | 71 | XCTAssertEqual(sut.showsExpansionTextWhenTruncated, true) 72 | } 73 | 74 | func test_adjustsFontSizeToFitWidth_modiefiesLabelCorrectly() { 75 | let sut = UILabel() 76 | 77 | sut.adjustsFontSizeToFitWidth(true) 78 | 79 | XCTAssertEqual(sut.adjustsFontSizeToFitWidth, true) 80 | } 81 | func test_allowsDefaultTighteningForTruncation_modiefiesLabelCorrectly() { 82 | let sut = UILabel() 83 | 84 | sut.allowsDefaultTighteningForTruncation(true) 85 | 86 | XCTAssertEqual(sut.allowsDefaultTighteningForTruncation, true) 87 | } 88 | 89 | func test_baselineAdjustment_modiefiesLabelCorrectly() { 90 | let sut = UILabel() 91 | 92 | sut.baselineAdjustment(.alignBaselines) 93 | 94 | XCTAssertEqual(sut.baselineAdjustment, .alignBaselines) 95 | } 96 | 97 | func test_minimumScaleFactor_modiefiesLabelCorrectly() { 98 | let sut = UILabel() 99 | 100 | sut.minimumScaleFactor(0.1) 101 | 102 | XCTAssertEqual(sut.minimumScaleFactor, 0.1, accuracy: 0.01) 103 | } 104 | 105 | func test_numberOfLines_modiefiesLabelCorrectly() { 106 | let sut = UILabel() 107 | 108 | sut.numberOfLines(0) 109 | 110 | XCTAssertEqual(sut.numberOfLines, 0) 111 | } 112 | 113 | func test_highlightedTextColor_modiefiesLabelCorrectly() { 114 | let sut = UILabel() 115 | 116 | sut.highlightedTextColor(.green) 117 | 118 | XCTAssertEqual(sut.highlightedTextColor, .green) 119 | } 120 | 121 | func test_isHighlighted_modiefiesLabelCorrectly() { 122 | let sut = UILabel() 123 | 124 | sut.isHighlighted(true) 125 | 126 | XCTAssertEqual(sut.isHighlighted, true) 127 | } 128 | 129 | func test_shadowColor_modiefiesLabelCorrectly() { 130 | let sut = UILabel() 131 | 132 | sut.shadowColor(.blue) 133 | 134 | XCTAssertEqual(sut.shadowColor, .blue) 135 | } 136 | 137 | func test_shadowOffset_modiefiesLabelCorrectly() { 138 | let sut = UILabel() 139 | 140 | sut.shadowOffset(.init(width: 123, height: 321)) 141 | 142 | XCTAssertEqual(sut.shadowOffset, .init(width: 123, height: 321)) 143 | } 144 | 145 | func test_preferredMaxLayoutWidth_modiefiesLabelCorrectly() { 146 | let sut = UILabel() 147 | 148 | sut.preferredMaxLayoutWidth(123) 149 | 150 | XCTAssertEqual(sut.preferredMaxLayoutWidth, 123) 151 | } 152 | 153 | func test_isUserInteractionEnabled_modiefiesLabelCorrectly() { 154 | let sut = UILabel() 155 | 156 | sut.isUserInteractionEnabled(false) 157 | 158 | XCTAssertEqual(sut.isUserInteractionEnabled, false) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Sources/ViewKit/Components/ViewController.swift: -------------------------------------------------------------------------------- 1 | protocol ViewControllerDelegate { 2 | func loadView() 3 | func viewDidLoad() 4 | func viewWillLayoutSubviews() 5 | func viewDidLayoutSubviews() 6 | func viewWillAppear(_ animated: Bool) 7 | func viewDidAppear(_ animated: Bool) 8 | func viewWillDisappear(_ animated: Bool) 9 | func viewDidDisappear(_ animated: Bool) 10 | func didBeginPresses(_ presses: Set, with event: UIPressesEvent?) 11 | func didEndPresses(_ presses: Set, with event: UIPressesEvent?) 12 | func didCancelPresses(_ presses: Set, with event: UIPressesEvent?) 13 | func didChangePresses(_ presses: Set, with event: UIPressesEvent?) 14 | } 15 | 16 | extension ViewControllerDelegate { 17 | func loadView() {} 18 | func viewDidLoad() {} 19 | func viewWillLayoutSubviews() {} 20 | func viewDidLayoutSubviews() {} 21 | func viewWillAppear(_ animated: Bool) {} 22 | func viewDidAppear(_ animated: Bool) {} 23 | func viewWillDisappear(_ animated: Bool) {} 24 | func viewDidDisappear(_ animated: Bool) {} 25 | func didBeginPresses(_ presses: Set, with event: UIPressesEvent?) {} 26 | func didEndPresses(_ presses: Set, with event: UIPressesEvent?) {} 27 | func didCancelPresses(_ presses: Set, with event: UIPressesEvent?) {} 28 | func didChangePresses(_ presses: Set, with event: UIPressesEvent?) {} 29 | } 30 | 31 | open class ContainerView: UIView { 32 | 33 | public init(for view: ProgrammaticView) { 34 | super.init(frame: .zero) 35 | 36 | let body = view.body 37 | 38 | body.prepareForConstraints() 39 | 40 | addingSubview(body) 41 | 42 | constrain([ 43 | body.topAnchor.constraint(greaterThanOrEqualTo: topAnchor), 44 | body.leftAnchor.constraint(greaterThanOrEqualTo: leftAnchor), 45 | body.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor), 46 | body.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor), 47 | body.centerXAnchor.constraint(equalTo: centerXAnchor), 48 | body.centerYAnchor.constraint(equalTo: centerYAnchor), 49 | ]) 50 | 51 | if #available(iOS 13.0, *) { 52 | backgroundColor = .systemBackground 53 | } 54 | } 55 | 56 | @available(*, unavailable) 57 | public required init?(coder: NSCoder) { 58 | fatalError("init(coder:) has not been implemented") 59 | } 60 | 61 | deinit { 62 | NotificationCenter.default.removeObserver(self) 63 | } 64 | } 65 | 66 | open class ViewController: UIViewController { 67 | var delegate: ViewControllerDelegate? 68 | var hideNavigationBar: Bool = false 69 | let contentView: UIView 70 | 71 | public init(with view: ProgrammaticView) { 72 | self.contentView = ContainerView(for: view) 73 | super.init(nibName: nil, bundle: nil) 74 | notify(.ProgrammaticViewContentUpdated) 75 | } 76 | 77 | @available(*, unavailable) 78 | public required init?(coder: NSCoder) { 79 | fatalError("init(coder:) has not been implemented") 80 | } 81 | 82 | public override func loadView() { 83 | super.loadView() 84 | self.view = contentView 85 | delegate?.loadView() 86 | } 87 | 88 | public override func viewDidLoad() { 89 | super.viewDidLoad() 90 | delegate?.viewDidLoad() 91 | } 92 | 93 | public override func viewWillLayoutSubviews() { 94 | super.viewWillLayoutSubviews() 95 | delegate?.viewWillLayoutSubviews() 96 | } 97 | 98 | public override func viewDidLayoutSubviews() { 99 | super.viewDidLayoutSubviews() 100 | delegate?.viewDidLayoutSubviews() 101 | } 102 | 103 | public override func viewWillAppear(_ animated: Bool) { 104 | super.viewWillAppear(animated) 105 | delegate?.viewWillAppear(animated) 106 | navigationController?.isNavigationBarHidden = hideNavigationBar 107 | } 108 | 109 | public override func viewDidAppear(_ animated: Bool) { 110 | super.viewDidAppear(animated) 111 | delegate?.viewDidAppear(animated) 112 | } 113 | 114 | public override func viewWillDisappear(_ animated: Bool) { 115 | super.viewWillDisappear(animated) 116 | delegate?.viewWillDisappear(animated) 117 | navigationController?.isNavigationBarHidden = hideNavigationBar 118 | } 119 | 120 | public override func viewDidDisappear(_ animated: Bool) { 121 | super.viewDidDisappear(animated) 122 | delegate?.viewDidDisappear(animated) 123 | } 124 | 125 | public override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { 126 | super.pressesBegan(presses, with: event) 127 | delegate?.didBeginPresses(presses, with: event) 128 | } 129 | 130 | public override func pressesEnded(_ presses: Set, with event: UIPressesEvent?) { 131 | super.pressesEnded(presses, with: event) 132 | delegate?.didEndPresses(presses, with: event) 133 | } 134 | 135 | public override func pressesCancelled(_ presses: Set, with event: UIPressesEvent?) { 136 | super.pressesCancelled(presses, with: event) 137 | delegate?.didCancelPresses(presses, with: event) 138 | } 139 | 140 | public override func pressesChanged(_ presses: Set, with event: UIPressesEvent?) { 141 | super.pressesChanged(presses, with: event) 142 | delegate?.didChangePresses(presses, with: event) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Sources/ViewKit/Extensions/UIButton+extensions.swift: -------------------------------------------------------------------------------- 1 | public extension UIButton { 2 | 3 | // MARK: - State 4 | 5 | @discardableResult 6 | func disabled(_ disabled: Bool = true) -> Self { 7 | self.isEnabled = !disabled 8 | return self 9 | } 10 | 11 | @discardableResult 12 | func enabled(_ enabled: Bool = true) -> Self { 13 | self.isEnabled = enabled 14 | return self 15 | } 16 | 17 | // MARK: - Configuration 18 | 19 | @available(iOS 15, *) 20 | @discardableResult 21 | func configuration(_ configuration: UIButton.Configuration) -> Self { 22 | self.configuration = configuration 23 | return self 24 | } 25 | 26 | @available(iOS 15, *) 27 | @discardableResult 28 | func automaticallyUpdatesConfiguration(_ automaticallyUpdatesConfiguration: Bool) -> Self { 29 | self.automaticallyUpdatesConfiguration = automaticallyUpdatesConfiguration 30 | return self 31 | } 32 | 33 | @available(iOS 15, *) 34 | @discardableResult 35 | func configurationUpdateHandler(_ configurationUpdateHandler: UIButton.ConfigurationUpdateHandler?) -> Self { 36 | self.configurationUpdateHandler = configurationUpdateHandler 37 | return self 38 | } 39 | 40 | // MARK: - Managing the title 41 | 42 | @discardableResult 43 | func title(_ title: String?, for state: UIControl.State = .normal) -> Self { 44 | self.setTitle(title, for: state) 45 | return self 46 | } 47 | 48 | @discardableResult 49 | func attributedTitle(_ title: NSAttributedString?, for state: UIControl.State = .normal) -> Self { 50 | self.setAttributedTitle(title, for: state) 51 | return self 52 | } 53 | 54 | @discardableResult 55 | func titleColor(_ color: UIColor?, for state: UIControl.State = .normal) -> Self { 56 | self.setTitleColor(color, for: state) 57 | return self 58 | } 59 | 60 | @discardableResult 61 | func titleShadowColor(_ color: UIColor?, for state: UIControl.State = .normal) -> Self { 62 | self.setTitleShadowColor(color, for: state) 63 | return self 64 | } 65 | 66 | // MARK: - Managing images and tint color 67 | 68 | @discardableResult 69 | func backgroundImage(_ backgroundImage: UIImage?, for state: UIControl.State) -> Self { 70 | self.setBackgroundImage(backgroundImage, for: state) 71 | return self 72 | } 73 | 74 | @discardableResult 75 | func image(_ image: UIImage?, for state: UIControl.State = .normal) -> Self { 76 | self.setImage(image, for: state) 77 | return self 78 | } 79 | 80 | @available(iOS 13.0, *) 81 | @discardableResult 82 | func image(systemName: String, for state: UIControl.State = .normal) -> Self { 83 | self.setImage(UIImage(systemName: systemName), for: state) 84 | return self 85 | } 86 | 87 | @discardableResult 88 | func image(_ name: String, for state: UIControl.State = .normal) -> Self { 89 | self.setImage(UIImage(named: name), for: state) 90 | return self 91 | } 92 | 93 | @available(iOS 13.0, *) 94 | @discardableResult 95 | func preferredSymbolConfiguration(_ preferredSymbolConfiguration: UIImage.SymbolConfiguration?, forImageIn state: UIControl.State) -> Self { 96 | self.setPreferredSymbolConfiguration(preferredSymbolConfiguration, forImageIn: state) 97 | return self 98 | } 99 | 100 | // MARK: - Specifying the role 101 | 102 | @available(iOS 14.0, *) 103 | @discardableResult 104 | func role(_ role: UIButton.Role) -> Self { 105 | self.role = role 106 | return self 107 | } 108 | 109 | // MARK: - Specifying the behavioral style 110 | 111 | @available(iOS 15, *) 112 | @discardableResult 113 | func preferredBehavioralStyle(_ preferredBehavioralStyle: UIBehavioralStyle) -> Self { 114 | self.preferredBehavioralStyle = preferredBehavioralStyle 115 | return self 116 | } 117 | 118 | // MARK: - Supporting menu and toggle buttons 119 | 120 | @available(iOS 14.0, *) 121 | @discardableResult 122 | func menu(_ menu: UIMenu?) -> Self { 123 | self.menu = menu 124 | return self 125 | } 126 | 127 | @available(iOS 15, *) 128 | @discardableResult 129 | func changesSelectionAsPrimaryAction(_ changesSelectionAsPrimaryAction: Bool) -> Self { 130 | self.changesSelectionAsPrimaryAction = changesSelectionAsPrimaryAction 131 | return self 132 | } 133 | 134 | @available(iOS 16, *) 135 | @discardableResult 136 | func preferredMenuElementOrder(_ order: UIContextMenuConfiguration.ElementOrder) -> Self { 137 | self.preferredMenuElementOrder = order 138 | return self 139 | } 140 | 141 | // MARK: - Action 142 | 143 | @available(iOS 14.0, *) 144 | @discardableResult 145 | func action(_ action: @escaping (UIButton) -> Void) -> Self { 146 | addAction(UIAction { _ in 147 | action(self) 148 | }, for: .touchUpInside) 149 | return self 150 | } 151 | } 152 | 153 | public extension UIButton { 154 | convenience init(_ title: String) { 155 | self.init(frame: .zero) 156 | self.setTitle(title, for: .normal) 157 | } 158 | } 159 | 160 | // MARK: - Title Label 161 | 162 | public extension UIButton { 163 | 164 | @discardableResult 165 | func text(_ text: String?) -> Self { 166 | titleLabel?.text = text 167 | return self 168 | } 169 | 170 | @discardableResult 171 | func attributedText(_ attributedText: NSAttributedString?) -> Self { 172 | titleLabel?.attributedText = attributedText 173 | return self 174 | } 175 | 176 | @discardableResult 177 | func font(_ font: UIFont?) -> Self { 178 | titleLabel?.font = font 179 | return self 180 | } 181 | 182 | @discardableResult 183 | func textColor(_ textColor: UIColor?) -> Self { 184 | titleLabel?.textColor = textColor 185 | return self 186 | } 187 | 188 | @discardableResult 189 | func textAlignment(_ textAlignment: NSTextAlignment) -> Self { 190 | titleLabel?.textAlignment = textAlignment 191 | return self 192 | } 193 | 194 | @discardableResult 195 | func lineBreakMode(_ lineBreakMode: NSLineBreakMode) -> Self { 196 | titleLabel?.lineBreakMode = lineBreakMode 197 | return self 198 | } 199 | 200 | @available(iOS 14.0, *) 201 | @discardableResult 202 | func lineBreakStrategy(_ lineBreakStrategy: NSParagraphStyle.LineBreakStrategy) -> Self { 203 | titleLabel?.lineBreakStrategy = lineBreakStrategy 204 | return self 205 | } 206 | 207 | @discardableResult 208 | func showsExpansionTextWhenTruncated(_ showsExpansionTextWhenTruncated: Bool) -> Self { 209 | titleLabel?.showsExpansionTextWhenTruncated = showsExpansionTextWhenTruncated 210 | return self 211 | } 212 | 213 | // MARK: - Sizing text 214 | 215 | @discardableResult 216 | func adjustsFontSizeToFitWidth(_ adjustsFontSizeToFitWidth: Bool) -> Self { 217 | titleLabel?.adjustsFontSizeToFitWidth = adjustsFontSizeToFitWidth 218 | return self 219 | } 220 | 221 | @discardableResult 222 | func allowsDefaultTighteningForTruncation(_ allowsDefaultTighteningForTruncation: Bool) -> Self { 223 | titleLabel?.allowsDefaultTighteningForTruncation = allowsDefaultTighteningForTruncation 224 | return self 225 | } 226 | 227 | @discardableResult 228 | func baselineAdjustment(_ baselineAdjustment: UIBaselineAdjustment) -> Self { 229 | titleLabel?.baselineAdjustment = baselineAdjustment 230 | return self 231 | } 232 | 233 | @discardableResult 234 | func minimumScaleFactor(_ minimumScaleFactor: CGFloat) -> Self { 235 | titleLabel?.minimumScaleFactor = minimumScaleFactor 236 | return self 237 | } 238 | 239 | @discardableResult 240 | func numberOfLines(_ numberOfLines: Int) -> Self { 241 | titleLabel?.numberOfLines = numberOfLines 242 | return self 243 | } 244 | 245 | // MARK: - Highlight 246 | 247 | @discardableResult 248 | func highlightedTextColor(_ highlightedTextColor: UIColor?) -> Self { 249 | titleLabel?.highlightedTextColor = highlightedTextColor 250 | return self 251 | } 252 | 253 | // MARK: - Shadow 254 | 255 | @discardableResult 256 | func shadowColor(_ shadowColor: UIColor?) -> Self { 257 | titleLabel?.shadowColor = shadowColor 258 | return self 259 | } 260 | 261 | @discardableResult 262 | func shadowOffset(_ shadowOffset: CGSize) -> Self { 263 | titleLabel?.shadowOffset = shadowOffset 264 | return self 265 | } 266 | 267 | @discardableResult 268 | func preferredMaxLayoutWidth(_ preferredMaxLayoutWidth: CGFloat) -> Self { 269 | titleLabel?.preferredMaxLayoutWidth = preferredMaxLayoutWidth 270 | return self 271 | } 272 | 273 | } 274 | -------------------------------------------------------------------------------- /Sources/ViewKit/Extensions/UITextField+extensions.swift: -------------------------------------------------------------------------------- 1 | public extension UITextField { 2 | 3 | // MARK: - Text Attributes 4 | 5 | @discardableResult 6 | func text(_ text: String?) -> Self { 7 | self.text = text 8 | return self 9 | } 10 | 11 | @discardableResult 12 | func attributedText(_ attributedText: NSAttributedString?) -> Self { 13 | self.attributedText = attributedText 14 | return self 15 | } 16 | 17 | @discardableResult 18 | func font(_ font: UIFont?) -> Self { 19 | self.font = font 20 | return self 21 | } 22 | 23 | @discardableResult 24 | func textColor(_ textColor: UIColor?) -> Self { 25 | self.textColor = textColor 26 | return self 27 | } 28 | 29 | @discardableResult 30 | func textAlignment(_ textAlignment: NSTextAlignment) -> Self { 31 | self.textAlignment = textAlignment 32 | return self 33 | } 34 | 35 | @discardableResult 36 | func placeholder(_ placeholder: String) -> Self { 37 | self.placeholder = placeholder 38 | return self 39 | } 40 | 41 | @discardableResult 42 | func clearsOnBeginEditing(_ clearsOnBeginEditing: Bool) -> Self { 43 | self.clearsOnBeginEditing = clearsOnBeginEditing 44 | return self 45 | } 46 | 47 | @discardableResult 48 | func clearsOnInsertion(_ clearsOnInsertion: Bool) -> Self { 49 | self.clearsOnInsertion = clearsOnInsertion 50 | return self 51 | } 52 | 53 | @discardableResult 54 | func delegate(_ delegate: UITextFieldDelegate) -> Self { 55 | self.delegate = delegate 56 | return self 57 | } 58 | 59 | @discardableResult 60 | func autocapitalizationType(_ autocapitalizationType: UITextAutocapitalizationType) -> Self { 61 | self.autocapitalizationType = autocapitalizationType 62 | return self 63 | } 64 | 65 | @discardableResult 66 | func autocorrectionType(_ autocorrectionType: UITextAutocorrectionType) -> Self { 67 | self.autocorrectionType = autocorrectionType 68 | return self 69 | } 70 | 71 | @discardableResult 72 | func spellCheckingType(_ spellCheckingType: UITextSpellCheckingType) -> Self { 73 | self.spellCheckingType = spellCheckingType 74 | return self 75 | } 76 | 77 | @discardableResult 78 | func isSecureTextEntry(_ isSecureTextEntry: Bool) -> Self { 79 | self.isSecureTextEntry = isSecureTextEntry 80 | return self 81 | } 82 | 83 | @discardableResult 84 | func secureEntry() -> Self { 85 | self.isSecureTextEntry = true 86 | return self 87 | } 88 | 89 | @discardableResult 90 | func rightView(_ rightView: UIView?) -> Self { 91 | self.rightView = rightView 92 | return self 93 | } 94 | 95 | @discardableResult 96 | func rightView(@ProgrammaticViewBuilder _ builder: (UITextField) -> UIView) -> Self { 97 | self.rightView(builder(self)) 98 | return self 99 | } 100 | 101 | @discardableResult 102 | func rightViewMode(_ rightViewMode: UITextField.ViewMode) -> Self { 103 | self.rightViewMode = rightViewMode 104 | return self 105 | } 106 | 107 | // MARK: - Sizing text 108 | 109 | @discardableResult 110 | func adjustsFontSizeToFitWidth(_ adjustsFontSizeToFitWidth: Bool) -> Self { 111 | self.adjustsFontSizeToFitWidth = adjustsFontSizeToFitWidth 112 | return self 113 | } 114 | 115 | // MARK: - Managing the editing behavior 116 | 117 | @discardableResult 118 | func allowsEditingTextAttributes(_ allowsEditingTextAttributes: Bool = true) -> Self { 119 | self.allowsEditingTextAttributes = allowsEditingTextAttributes 120 | return self 121 | } 122 | 123 | // MARK: - Background appearance 124 | 125 | @discardableResult 126 | func borderStyle(_ borderStyle: BorderStyle) -> Self { 127 | self.borderStyle = borderStyle 128 | return self 129 | } 130 | 131 | @discardableResult 132 | func background(_ background: UIImage?) -> Self { 133 | self.background = background 134 | return self 135 | } 136 | 137 | @discardableResult 138 | func disabledBackground(_ disabledBackground: UIImage?) -> Self { 139 | self.disabledBackground = disabledBackground 140 | return self 141 | } 142 | 143 | // MARK: - Overlay 144 | 145 | @discardableResult 146 | func clearButtonMode(_ clearButtonMode: ViewMode) -> Self { 147 | self.clearButtonMode = clearButtonMode 148 | return self 149 | } 150 | 151 | @discardableResult 152 | func leftView(_ leftView: UIView?) -> Self { 153 | self.leftView = leftView 154 | return self 155 | } 156 | 157 | @discardableResult 158 | func leftViewMode(_ leftViewMode: ViewMode) -> Self { 159 | self.leftViewMode = leftViewMode 160 | return self 161 | } 162 | 163 | @discardableResult 164 | func inputView(_ inputView: UIView?) -> Self { 165 | self.inputView = inputView 166 | return self 167 | } 168 | 169 | @discardableResult 170 | func inputAccessoryView(_ inputAccessoryView: UIView?) -> Self { 171 | self.inputAccessoryView = inputAccessoryView 172 | return self 173 | } 174 | 175 | @available(iOS 15.0, *) 176 | @discardableResult 177 | func interactionState(_ interactionState: Any) -> Self { 178 | self.interactionState = interactionState 179 | return self 180 | } 181 | 182 | // MARK: - Tag 183 | 184 | @discardableResult 185 | func tag(_ tag: Int) -> Self { 186 | self.tag = tag 187 | return self 188 | } 189 | } 190 | 191 | public extension UITextField { 192 | enum AssociatedKeys { 193 | static var delegateClosure = "delegateClosure" 194 | static var shouldBeginEditingClosure = "shouldBeginEditingClosure" 195 | static var didBeginEditingClosure = "didBeginEditingClosure" 196 | static var shouldEndEditingClosure = "shouldEndEditingClosure" 197 | static var didEndEditingClosure = "didEndEditingClosure" 198 | static var shouldChangeCharactersClosure = "shouldChangeCharactersClosure" 199 | static var shouldClearClosure = "shouldClearClosure" 200 | static var onChangeClosure = "onChangeClosure" 201 | } 202 | 203 | private var delegateClosure: ((UITextField) -> Bool)? { 204 | get { 205 | return objc_getAssociatedObject(self, &AssociatedKeys.delegateClosure) as? (UITextField) -> Bool 206 | } 207 | set { 208 | objc_setAssociatedObject(self, &AssociatedKeys.delegateClosure, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 209 | } 210 | } 211 | 212 | private var shouldBeginEditingClosure: ((UITextField) -> Bool)? { 213 | get { 214 | return objc_getAssociatedObject(self, &AssociatedKeys.shouldBeginEditingClosure) as? (UITextField) -> Bool 215 | } 216 | set { 217 | objc_setAssociatedObject(self, &AssociatedKeys.shouldBeginEditingClosure, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 218 | } 219 | } 220 | 221 | private var didBeginEditingClosure: ((UITextField) -> Void)? { 222 | get { 223 | return objc_getAssociatedObject(self, &AssociatedKeys.didBeginEditingClosure) as? (UITextField) -> Void 224 | } 225 | set { 226 | objc_setAssociatedObject(self, &AssociatedKeys.didBeginEditingClosure, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 227 | } 228 | } 229 | 230 | private var shouldEndEditingClosure: ((UITextField) -> Bool)? { 231 | get { 232 | return objc_getAssociatedObject(self, &AssociatedKeys.shouldEndEditingClosure) as? (UITextField) -> Bool 233 | } 234 | set { 235 | objc_setAssociatedObject(self, &AssociatedKeys.shouldEndEditingClosure, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 236 | } 237 | } 238 | 239 | private var didEndEditingClosure: ((UITextField) -> Void)? { 240 | get { 241 | return objc_getAssociatedObject(self, &AssociatedKeys.didEndEditingClosure) as? (UITextField) -> Void 242 | } 243 | set { 244 | objc_setAssociatedObject(self, &AssociatedKeys.didEndEditingClosure, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 245 | } 246 | } 247 | 248 | private var shouldChangeCharactersClosure: ((UITextField, NSRange, String) -> Bool)? { 249 | get { 250 | return objc_getAssociatedObject(self, &AssociatedKeys.shouldChangeCharactersClosure) as? (UITextField, NSRange, String) -> Bool 251 | } 252 | set { 253 | objc_setAssociatedObject(self, &AssociatedKeys.shouldChangeCharactersClosure, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 254 | } 255 | } 256 | 257 | private var shouldClearClosure: ((UITextField) -> Bool)? { 258 | get { 259 | return objc_getAssociatedObject(self, &AssociatedKeys.shouldClearClosure) as? (UITextField) -> Bool 260 | } 261 | set { 262 | objc_setAssociatedObject(self, &AssociatedKeys.shouldClearClosure, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 263 | } 264 | } 265 | 266 | private var onChangeClosure: ((UITextField) -> Void)? { 267 | get { 268 | return objc_getAssociatedObject(self, &AssociatedKeys.onChangeClosure) as? (UITextField) -> Void 269 | } 270 | set { 271 | objc_setAssociatedObject(self, &AssociatedKeys.onChangeClosure, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 272 | } 273 | } 274 | 275 | func onBeginEditing(_ closure: @escaping (UITextField) -> Void) -> Self { 276 | didBeginEditingClosure = closure 277 | delegate = self 278 | return self 279 | } 280 | 281 | func shouldEndEditing(_ closure: @escaping (UITextField) -> Bool) -> Self { 282 | shouldEndEditingClosure = closure 283 | delegate = self 284 | return self 285 | } 286 | 287 | func onEndEditing(_ closure: @escaping (UITextField) -> Void) -> Self { 288 | didEndEditingClosure = closure 289 | delegate = self 290 | return self 291 | } 292 | 293 | func shouldChangeCharacters(_ closure: @escaping (UITextField, NSRange, String) -> Bool) -> Self { 294 | shouldChangeCharactersClosure = closure 295 | delegate = self 296 | return self 297 | } 298 | 299 | func shouldClear(_ closure: @escaping (UITextField) -> Bool) -> Self { 300 | shouldClearClosure = closure 301 | delegate = self 302 | return self 303 | } 304 | 305 | func shouldReturn(_ closure: @escaping (UITextField) -> Bool) -> Self { 306 | delegateClosure = closure 307 | delegate = self 308 | return self 309 | } 310 | 311 | func onChange(_ closure: @escaping (UITextField) -> Void) -> Self { 312 | addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged) 313 | onChangeClosure = closure 314 | return self 315 | } 316 | 317 | @objc private func textFieldDidChange() { 318 | onChangeClosure?(self) 319 | } 320 | } 321 | 322 | extension UITextField: UITextFieldDelegate { 323 | 324 | public func textFieldShouldReturn(_ textField: UITextField) -> Bool { 325 | return delegateClosure?(textField) ?? true 326 | } 327 | 328 | public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { 329 | return shouldBeginEditingClosure?(textField) ?? true 330 | } 331 | 332 | public func textFieldDidBeginEditing(_ textField: UITextField) { 333 | didBeginEditingClosure?(textField) 334 | } 335 | 336 | public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { 337 | return shouldEndEditingClosure?(textField) ?? true 338 | } 339 | 340 | public func textFieldDidEndEditing(_ textField: UITextField) { 341 | didEndEditingClosure?(textField) 342 | } 343 | 344 | public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 345 | return shouldChangeCharactersClosure?(textField, range, string) ?? true 346 | } 347 | 348 | public func textFieldShouldClear(_ textField: UITextField) -> Bool { 349 | return shouldClearClosure?(textField) ?? true 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /Sources/ViewKit/Extensions/UIView+extensions.swift: -------------------------------------------------------------------------------- 1 | public extension UIView { 2 | enum Edge { 3 | case top 4 | case left 5 | case bottom 6 | case right 7 | 8 | case horizontal 9 | case vertical 10 | case all 11 | } 12 | 13 | convenience init(_ color: UIColor) { 14 | self.init() 15 | backgroundColor(color) 16 | maxSize() 17 | } 18 | 19 | // MARK: - Layout 20 | 21 | @discardableResult 22 | func addingSubview(_ view: UIView) -> Self { 23 | addSubview(view) 24 | return self 25 | } 26 | 27 | func prepareForConstraints() { 28 | translatesAutoresizingMaskIntoConstraints = false 29 | } 30 | 31 | @discardableResult 32 | func preparedForConstraints() -> Self { 33 | translatesAutoresizingMaskIntoConstraints = false 34 | return self 35 | } 36 | 37 | @discardableResult 38 | func prepareSubviewsForConstraints() -> Self { 39 | subviews.forEach { $0.prepareForConstraints() } 40 | return self 41 | } 42 | 43 | @discardableResult 44 | func constrained(_ constraints: [NSLayoutConstraint]) -> Self { 45 | self.translatesAutoresizingMaskIntoConstraints = false 46 | NSLayoutConstraint.activate(constraints) 47 | return self 48 | } 49 | 50 | @discardableResult 51 | func constrain(_ constraints: [NSLayoutConstraint]) -> Self { 52 | NSLayoutConstraint.activate(constraints) 53 | return self 54 | } 55 | 56 | // MARK: - Configuring a view's visual apperance 57 | 58 | @discardableResult 59 | func backgroundColor(_ color: UIColor?) -> Self { 60 | backgroundColor = color 61 | return self 62 | } 63 | 64 | @discardableResult 65 | func hidden(_ isHidden: Bool = true) -> Self { 66 | self.isHidden = isHidden 67 | return self 68 | } 69 | 70 | @discardableResult 71 | func show(_ isShowing: Bool = true) -> Self { 72 | self.isHidden = !isShowing 73 | return self 74 | } 75 | 76 | @discardableResult 77 | func alpha(_ value: CGFloat) -> Self { 78 | alpha = value 79 | return self 80 | } 81 | 82 | @discardableResult 83 | func opacity(_ value: CGFloat) -> Self { 84 | return alpha(value) 85 | } 86 | 87 | @discardableResult 88 | func translucent() -> Self { 89 | return alpha(0.5) 90 | } 91 | 92 | @discardableResult 93 | func transparent() -> Self { 94 | return alpha(0) 95 | } 96 | 97 | @discardableResult 98 | func isOpaque(_ isOpaque: Bool) -> Self { 99 | self.isOpaque = isOpaque 100 | return self 101 | } 102 | 103 | @discardableResult 104 | func tintColor(_ tintColor: UIColor?) -> Self { 105 | self.tintColor = tintColor 106 | return self 107 | } 108 | 109 | @discardableResult 110 | func tintAdjustmentsMode(_ tintAdjustmentsMode: TintAdjustmentMode) -> Self { 111 | self.tintAdjustmentMode = tintAdjustmentsMode 112 | return self 113 | } 114 | 115 | @discardableResult 116 | func clipsToBounds(_ clipsToBounds: Bool = true) -> Self { 117 | self.clipsToBounds = clipsToBounds 118 | return self 119 | } 120 | 121 | @discardableResult 122 | func clearsContextBeforeDrawing(_ clearsContextBeforeDrawing: Bool = true) -> Self { 123 | self.clearsContextBeforeDrawing = clearsContextBeforeDrawing 124 | return self 125 | } 126 | 127 | @discardableResult 128 | func mask(_ mask: UIView?) -> Self { 129 | self.mask = mask 130 | return self 131 | } 132 | 133 | @discardableResult 134 | func isUserInteractionEnabled (_ isUserInteractionEnabled: Bool = true) -> Self { 135 | self.isUserInteractionEnabled = isUserInteractionEnabled 136 | return self 137 | } 138 | 139 | @discardableResult 140 | func isMultipleTouchEnabled (_ isMultipleTouchEnabled: Bool = true) -> Self { 141 | self.isMultipleTouchEnabled = isMultipleTouchEnabled 142 | return self 143 | } 144 | 145 | @discardableResult 146 | func isExclusiveTouch (_ isExclusiveTouch: Bool = true) -> Self { 147 | self.isExclusiveTouch = isExclusiveTouch 148 | return self 149 | } 150 | 151 | @discardableResult 152 | func cornerRadius(_ radius: CGFloat) -> Self { 153 | layer.cornerRadius = radius 154 | return self 155 | } 156 | 157 | @discardableResult 158 | func maskToBounds(_ maskToBounds: Bool = true) -> Self { 159 | layer.masksToBounds = maskToBounds 160 | return self 161 | } 162 | 163 | // MARK: - Bounds and frames 164 | 165 | @discardableResult 166 | func frame(_ frame: CGRect) -> Self { 167 | self.frame = frame 168 | return self 169 | } 170 | 171 | @discardableResult 172 | func bounds(_ bounds: CGRect) -> Self { 173 | self.bounds = bounds 174 | return self 175 | } 176 | 177 | @discardableResult 178 | func contentHuggingPriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) -> Self { 179 | self.setContentHuggingPriority(priority, for: axis) 180 | return self 181 | } 182 | 183 | @discardableResult 184 | func compressionResistancePriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis) -> Self { 185 | self.setContentCompressionResistancePriority(priority, for: axis) 186 | return self 187 | } 188 | 189 | @discardableResult 190 | func transform(_ scaleX: CGFloat, _ scaleY: CGFloat) -> Self { 191 | transform = CGAffineTransform(scaleX: scaleX, y: scaleY) 192 | return self 193 | } 194 | 195 | @discardableResult 196 | func scale(_ amount: CGFloat) -> Self { 197 | return transform(amount, amount) 198 | } 199 | 200 | @discardableResult 201 | func move(_ dx: CGFloat, _ dy: CGFloat = 0) -> Self { 202 | transform = transform.translatedBy(x: dx, y: dy) 203 | return self 204 | } 205 | 206 | @discardableResult 207 | func fit() -> Self { 208 | self.sizeToFit() 209 | return self 210 | } 211 | 212 | // MARK: - Accessbility 213 | 214 | func accessibilityIdentifier(_ identifier: String) -> Self { 215 | accessibilityIdentifier = identifier 216 | return self 217 | } 218 | 219 | @discardableResult 220 | func accessibilityFrame(_ frame: CGRect) -> Self { 221 | self.accessibilityFrame = frame 222 | return self 223 | } 224 | 225 | @discardableResult 226 | func accessibilityLabel(_ label: String) -> Self { 227 | self.accessibilityLabel = label 228 | return self 229 | } 230 | 231 | // MARK: - Gestures 232 | 233 | @discardableResult 234 | func onTap(numberOfTapsRequired: Int = 1, _ completion: @escaping (Self) -> Void) -> Self { 235 | let tapGesture = ClosureTapGestureRecognizer() 236 | tapGesture.numberOfTapsRequired = numberOfTapsRequired 237 | 238 | tapGesture.addAction { 239 | completion(self) 240 | } 241 | 242 | addGestureRecognizer(tapGesture) 243 | return self 244 | } 245 | 246 | @discardableResult 247 | func onLongPress(minimumPressDuration: TimeInterval = 0.5, _ completion: @escaping (Self) -> Void) -> Self { 248 | let longPressGesture = ClosureLongPressGestureRecognizer() 249 | longPressGesture.minimumPressDuration = minimumPressDuration 250 | 251 | longPressGesture.addAction { 252 | completion(self) 253 | } 254 | 255 | addGestureRecognizer(longPressGesture) 256 | return self 257 | } 258 | 259 | @discardableResult 260 | func onSwipe(direction: UISwipeGestureRecognizer.Direction = .right, _ completion: @escaping (Self) -> Void) -> Self { 261 | let swipeGesture = ClosureSwipeGestureRecognizer() 262 | swipeGesture.direction = direction 263 | 264 | swipeGesture.addAction { 265 | completion(self) 266 | } 267 | 268 | addGestureRecognizer(swipeGesture) 269 | return self 270 | } 271 | 272 | @discardableResult 273 | func onPinch(_ completion: @escaping (Self) -> Void) -> Self { 274 | let pinchGesture = ClosurePinchGestureRecognizer() 275 | 276 | pinchGesture.addAction { 277 | completion(self) 278 | } 279 | 280 | addGestureRecognizer(pinchGesture) 281 | return self 282 | } 283 | 284 | // MARK: - Gestures 285 | 286 | @discardableResult 287 | func onRotation(_ completion: @escaping (Self) -> Void) -> Self { 288 | let rotationGesture = ClosureRotationGestureRecognizer() 289 | 290 | rotationGesture.addAction { 291 | completion(self) 292 | } 293 | 294 | addGestureRecognizer(rotationGesture) 295 | return self 296 | } 297 | 298 | @discardableResult 299 | func onPan(minimumNumberOfTouches: Int = 1, maximumNumberOfTouches: Int = 2, _ completion: @escaping (Self) -> Void) -> Self { 300 | let panGesture = ClosurePanGestureRecognizer() 301 | panGesture.minimumNumberOfTouches = minimumNumberOfTouches 302 | panGesture.maximumNumberOfTouches = maximumNumberOfTouches 303 | 304 | panGesture.addAction { 305 | completion(self) 306 | } 307 | 308 | addGestureRecognizer(panGesture) 309 | return self 310 | } 311 | 312 | } 313 | 314 | public extension UIView { 315 | 316 | @discardableResult 317 | func width(_ constant: CGFloat) -> Self { 318 | return constrained([ 319 | widthAnchor.constraint(equalToConstant: constant) 320 | ]) 321 | } 322 | 323 | @discardableResult 324 | func height(_ constant: CGFloat) -> Self { 325 | return constrained([ 326 | heightAnchor.constraint(equalToConstant: constant) 327 | ]) 328 | } 329 | 330 | @discardableResult 331 | func size(_ constant: CGFloat) -> Self { 332 | return width(constant).height(constant) 333 | } 334 | 335 | @discardableResult 336 | func pinEdge(_ edge: Edge, _ offset: CGFloat = 0) -> Self { 337 | guard let superview else { return self } 338 | 339 | if [.top, .vertical, .all].contains(edge) { 340 | constrained([ topAnchor.constraint(equalTo: superview.topAnchor, constant: offset) ]) 341 | } 342 | 343 | if [.left, .horizontal, .all].contains(edge) { 344 | constrained([ leftAnchor.constraint(equalTo: superview.leftAnchor, constant: offset) ]) 345 | } 346 | 347 | if [.bottom, .vertical, .all].contains(edge) { 348 | constrained([ bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: -offset) ]) 349 | } 350 | 351 | if [.right, .horizontal, .all].contains(edge) { 352 | constrained([ rightAnchor.constraint(equalTo: superview.rightAnchor, constant: -offset) ]) 353 | } 354 | 355 | return self 356 | } 357 | 358 | @discardableResult 359 | func pinEdges(_ edges: [Edge] = [.all], _ offset: CGFloat = 0) -> Self { 360 | edges.forEach { pinEdge($0)} 361 | return self 362 | } 363 | 364 | @discardableResult 365 | func pinEdges(_ offset: CGFloat = 0) -> Self { 366 | pinEdge(.all, offset) 367 | return self 368 | } 369 | 370 | @discardableResult 371 | func maxWidth() -> Self { 372 | when(.ProgrammaticViewContentUpdated) { [weak self] in 373 | guard let self else { return } 374 | guard let superview = self.superview else { return } 375 | let constraint = self.widthAnchor.constraint(equalTo: superview.widthAnchor, multiplier: 1) 376 | constraint.priority = .dragThatCanResizeScene 377 | constraint.isActive = true 378 | } 379 | return self 380 | } 381 | 382 | @discardableResult 383 | func maxHeight() -> Self { 384 | when(.ProgrammaticViewContentUpdated) { [weak self] in 385 | guard let self else { return } 386 | guard let superview = self.superview else { return } 387 | let constraint = self.heightAnchor.constraint(equalTo: superview.heightAnchor, multiplier: 1) 388 | constraint.priority = .dragThatCanResizeScene 389 | constraint.isActive = true 390 | } 391 | return self 392 | } 393 | 394 | @discardableResult 395 | func maxSize() -> Self { 396 | return maxHeight().maxWidth() 397 | } 398 | 399 | @discardableResult 400 | func padding(_ padding: CGFloat = 10) -> UIView { 401 | let containerView = UIView() 402 | containerView.addSubview(self) 403 | pinEdges(padding) 404 | return containerView 405 | } 406 | 407 | @discardableResult 408 | func padding(_ edge: Edge, _ padding: CGFloat = 10) -> UIView { 409 | let containerView = UIView() 410 | containerView.addSubview(self) 411 | pinEdge(edge, padding) 412 | return containerView 413 | } 414 | 415 | func round() -> Self { 416 | layer.cornerRadius = min(bounds.width, bounds.height) / 2 417 | layer.masksToBounds = true 418 | addObserver(self, forKeyPath: #keyPath(bounds), options: .new, context: nil) 419 | return self 420 | } 421 | 422 | override func observeValue( 423 | forKeyPath keyPath: String?, 424 | of object: Any?, 425 | change: [NSKeyValueChangeKey: Any]?, 426 | context: UnsafeMutableRawPointer? 427 | ) { 428 | if keyPath == #keyPath(bounds) { 429 | layer.cornerRadius = min(bounds.width, bounds.height) / 2 430 | } 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /Sample/ViewKitSample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A068A82329B1FD91003BFD66 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A068A82229B1FD91003BFD66 /* AppDelegate.swift */; }; 11 | A068A82529B1FD91003BFD66 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A068A82429B1FD91003BFD66 /* SceneDelegate.swift */; }; 12 | A068A82C29B1FD92003BFD66 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A068A82B29B1FD92003BFD66 /* Assets.xcassets */; }; 13 | A068A82F29B1FD92003BFD66 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A068A82D29B1FD92003BFD66 /* LaunchScreen.storyboard */; }; 14 | A068A83A29B1FEEB003BFD66 /* ViewKit in Frameworks */ = {isa = PBXBuildFile; productRef = A068A83929B1FEEB003BFD66 /* ViewKit */; }; 15 | A0D1539429B670B300141606 /* SampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0D1539329B670B300141606 /* SampleView.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | A01EE87629BA2BF7002436CB /* ViewKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ViewKit; path = ..; sourceTree = ""; }; 20 | A068A81F29B1FD91003BFD66 /* ViewKitSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ViewKitSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | A068A82229B1FD91003BFD66 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | A068A82429B1FD91003BFD66 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 23 | A068A82B29B1FD92003BFD66 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | A068A82E29B1FD92003BFD66 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 25 | A068A83029B1FD92003BFD66 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 26 | A0D1539329B670B300141606 /* SampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleView.swift; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | A068A81C29B1FD91003BFD66 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | A068A83A29B1FEEB003BFD66 /* ViewKit in Frameworks */, 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | A068A81629B1FD91003BFD66 = { 42 | isa = PBXGroup; 43 | children = ( 44 | A068A83629B1FE9A003BFD66 /* Packages */, 45 | A068A82129B1FD91003BFD66 /* ViewKitSample */, 46 | A068A82029B1FD91003BFD66 /* Products */, 47 | A068A83829B1FEEB003BFD66 /* Frameworks */, 48 | ); 49 | sourceTree = ""; 50 | }; 51 | A068A82029B1FD91003BFD66 /* Products */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | A068A81F29B1FD91003BFD66 /* ViewKitSample.app */, 55 | ); 56 | name = Products; 57 | sourceTree = ""; 58 | }; 59 | A068A82129B1FD91003BFD66 /* ViewKitSample */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | A068A82229B1FD91003BFD66 /* AppDelegate.swift */, 63 | A068A82429B1FD91003BFD66 /* SceneDelegate.swift */, 64 | A068A82B29B1FD92003BFD66 /* Assets.xcassets */, 65 | A068A82D29B1FD92003BFD66 /* LaunchScreen.storyboard */, 66 | A068A83029B1FD92003BFD66 /* Info.plist */, 67 | A0D1539329B670B300141606 /* SampleView.swift */, 68 | ); 69 | path = ViewKitSample; 70 | sourceTree = ""; 71 | }; 72 | A068A83629B1FE9A003BFD66 /* Packages */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | A01EE87629BA2BF7002436CB /* ViewKit */, 76 | ); 77 | name = Packages; 78 | sourceTree = ""; 79 | }; 80 | A068A83829B1FEEB003BFD66 /* Frameworks */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | ); 84 | name = Frameworks; 85 | sourceTree = ""; 86 | }; 87 | /* End PBXGroup section */ 88 | 89 | /* Begin PBXNativeTarget section */ 90 | A068A81E29B1FD91003BFD66 /* ViewKitSample */ = { 91 | isa = PBXNativeTarget; 92 | buildConfigurationList = A068A83329B1FD92003BFD66 /* Build configuration list for PBXNativeTarget "ViewKitSample" */; 93 | buildPhases = ( 94 | A068A81B29B1FD91003BFD66 /* Sources */, 95 | A068A81C29B1FD91003BFD66 /* Frameworks */, 96 | A068A81D29B1FD91003BFD66 /* Resources */, 97 | ); 98 | buildRules = ( 99 | ); 100 | dependencies = ( 101 | ); 102 | name = ViewKitSample; 103 | packageProductDependencies = ( 104 | A068A83929B1FEEB003BFD66 /* ViewKit */, 105 | ); 106 | productName = ViewKitSample; 107 | productReference = A068A81F29B1FD91003BFD66 /* ViewKitSample.app */; 108 | productType = "com.apple.product-type.application"; 109 | }; 110 | /* End PBXNativeTarget section */ 111 | 112 | /* Begin PBXProject section */ 113 | A068A81729B1FD91003BFD66 /* Project object */ = { 114 | isa = PBXProject; 115 | attributes = { 116 | BuildIndependentTargetsInParallel = 1; 117 | LastSwiftUpdateCheck = 1420; 118 | LastUpgradeCheck = 1420; 119 | TargetAttributes = { 120 | A068A81E29B1FD91003BFD66 = { 121 | CreatedOnToolsVersion = 14.2; 122 | }; 123 | }; 124 | }; 125 | buildConfigurationList = A068A81A29B1FD91003BFD66 /* Build configuration list for PBXProject "ViewKitSample" */; 126 | compatibilityVersion = "Xcode 14.0"; 127 | developmentRegion = en; 128 | hasScannedForEncodings = 0; 129 | knownRegions = ( 130 | en, 131 | Base, 132 | ); 133 | mainGroup = A068A81629B1FD91003BFD66; 134 | productRefGroup = A068A82029B1FD91003BFD66 /* Products */; 135 | projectDirPath = ""; 136 | projectRoot = ""; 137 | targets = ( 138 | A068A81E29B1FD91003BFD66 /* ViewKitSample */, 139 | ); 140 | }; 141 | /* End PBXProject section */ 142 | 143 | /* Begin PBXResourcesBuildPhase section */ 144 | A068A81D29B1FD91003BFD66 /* Resources */ = { 145 | isa = PBXResourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | A068A82F29B1FD92003BFD66 /* LaunchScreen.storyboard in Resources */, 149 | A068A82C29B1FD92003BFD66 /* Assets.xcassets in Resources */, 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | }; 153 | /* End PBXResourcesBuildPhase section */ 154 | 155 | /* Begin PBXSourcesBuildPhase section */ 156 | A068A81B29B1FD91003BFD66 /* Sources */ = { 157 | isa = PBXSourcesBuildPhase; 158 | buildActionMask = 2147483647; 159 | files = ( 160 | A068A82329B1FD91003BFD66 /* AppDelegate.swift in Sources */, 161 | A0D1539429B670B300141606 /* SampleView.swift in Sources */, 162 | A068A82529B1FD91003BFD66 /* SceneDelegate.swift in Sources */, 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | /* End PBXSourcesBuildPhase section */ 167 | 168 | /* Begin PBXVariantGroup section */ 169 | A068A82D29B1FD92003BFD66 /* LaunchScreen.storyboard */ = { 170 | isa = PBXVariantGroup; 171 | children = ( 172 | A068A82E29B1FD92003BFD66 /* Base */, 173 | ); 174 | name = LaunchScreen.storyboard; 175 | sourceTree = ""; 176 | }; 177 | /* End PBXVariantGroup section */ 178 | 179 | /* Begin XCBuildConfiguration section */ 180 | A068A83129B1FD92003BFD66 /* Debug */ = { 181 | isa = XCBuildConfiguration; 182 | buildSettings = { 183 | ALWAYS_SEARCH_USER_PATHS = NO; 184 | CLANG_ANALYZER_NONNULL = YES; 185 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 186 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 187 | CLANG_ENABLE_MODULES = YES; 188 | CLANG_ENABLE_OBJC_ARC = YES; 189 | CLANG_ENABLE_OBJC_WEAK = YES; 190 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 191 | CLANG_WARN_BOOL_CONVERSION = YES; 192 | CLANG_WARN_COMMA = YES; 193 | CLANG_WARN_CONSTANT_CONVERSION = YES; 194 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 195 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 196 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 197 | CLANG_WARN_EMPTY_BODY = YES; 198 | CLANG_WARN_ENUM_CONVERSION = YES; 199 | CLANG_WARN_INFINITE_RECURSION = YES; 200 | CLANG_WARN_INT_CONVERSION = YES; 201 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 202 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 203 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 204 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 205 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 206 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 207 | CLANG_WARN_STRICT_PROTOTYPES = YES; 208 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 209 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 210 | CLANG_WARN_UNREACHABLE_CODE = YES; 211 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 212 | COPY_PHASE_STRIP = NO; 213 | DEBUG_INFORMATION_FORMAT = dwarf; 214 | ENABLE_STRICT_OBJC_MSGSEND = YES; 215 | ENABLE_TESTABILITY = YES; 216 | GCC_C_LANGUAGE_STANDARD = gnu11; 217 | GCC_DYNAMIC_NO_PIC = NO; 218 | GCC_NO_COMMON_BLOCKS = YES; 219 | GCC_OPTIMIZATION_LEVEL = 0; 220 | GCC_PREPROCESSOR_DEFINITIONS = ( 221 | "DEBUG=1", 222 | "$(inherited)", 223 | ); 224 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 225 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 226 | GCC_WARN_UNDECLARED_SELECTOR = YES; 227 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 228 | GCC_WARN_UNUSED_FUNCTION = YES; 229 | GCC_WARN_UNUSED_VARIABLE = YES; 230 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 231 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 232 | MTL_FAST_MATH = YES; 233 | ONLY_ACTIVE_ARCH = YES; 234 | SDKROOT = iphoneos; 235 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 236 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 237 | }; 238 | name = Debug; 239 | }; 240 | A068A83229B1FD92003BFD66 /* Release */ = { 241 | isa = XCBuildConfiguration; 242 | buildSettings = { 243 | ALWAYS_SEARCH_USER_PATHS = NO; 244 | CLANG_ANALYZER_NONNULL = YES; 245 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 246 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 247 | CLANG_ENABLE_MODULES = YES; 248 | CLANG_ENABLE_OBJC_ARC = YES; 249 | CLANG_ENABLE_OBJC_WEAK = YES; 250 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 251 | CLANG_WARN_BOOL_CONVERSION = YES; 252 | CLANG_WARN_COMMA = YES; 253 | CLANG_WARN_CONSTANT_CONVERSION = YES; 254 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 255 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 256 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 257 | CLANG_WARN_EMPTY_BODY = YES; 258 | CLANG_WARN_ENUM_CONVERSION = YES; 259 | CLANG_WARN_INFINITE_RECURSION = YES; 260 | CLANG_WARN_INT_CONVERSION = YES; 261 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 263 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 264 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 265 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 266 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 267 | CLANG_WARN_STRICT_PROTOTYPES = YES; 268 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 269 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 270 | CLANG_WARN_UNREACHABLE_CODE = YES; 271 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 272 | COPY_PHASE_STRIP = NO; 273 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 274 | ENABLE_NS_ASSERTIONS = NO; 275 | ENABLE_STRICT_OBJC_MSGSEND = YES; 276 | GCC_C_LANGUAGE_STANDARD = gnu11; 277 | GCC_NO_COMMON_BLOCKS = YES; 278 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 279 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 280 | GCC_WARN_UNDECLARED_SELECTOR = YES; 281 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 282 | GCC_WARN_UNUSED_FUNCTION = YES; 283 | GCC_WARN_UNUSED_VARIABLE = YES; 284 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 285 | MTL_ENABLE_DEBUG_INFO = NO; 286 | MTL_FAST_MATH = YES; 287 | SDKROOT = iphoneos; 288 | SWIFT_COMPILATION_MODE = wholemodule; 289 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 290 | VALIDATE_PRODUCT = YES; 291 | }; 292 | name = Release; 293 | }; 294 | A068A83429B1FD92003BFD66 /* Debug */ = { 295 | isa = XCBuildConfiguration; 296 | buildSettings = { 297 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 298 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 299 | CODE_SIGN_STYLE = Automatic; 300 | CURRENT_PROJECT_VERSION = 1; 301 | DEVELOPMENT_TEAM = AR7T5G5Z83; 302 | GENERATE_INFOPLIST_FILE = YES; 303 | INFOPLIST_FILE = ViewKitSample/Info.plist; 304 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 305 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 306 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 307 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 308 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 309 | LD_RUNPATH_SEARCH_PATHS = ( 310 | "$(inherited)", 311 | "@executable_path/Frameworks", 312 | ); 313 | MARKETING_VERSION = 1.0; 314 | PRODUCT_BUNDLE_IDENTIFIER = lucaswkuipers.ViewKitSample; 315 | PRODUCT_NAME = "$(TARGET_NAME)"; 316 | SWIFT_EMIT_LOC_STRINGS = YES; 317 | SWIFT_VERSION = 5.0; 318 | TARGETED_DEVICE_FAMILY = "1,2"; 319 | }; 320 | name = Debug; 321 | }; 322 | A068A83529B1FD92003BFD66 /* Release */ = { 323 | isa = XCBuildConfiguration; 324 | buildSettings = { 325 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 326 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 327 | CODE_SIGN_STYLE = Automatic; 328 | CURRENT_PROJECT_VERSION = 1; 329 | DEVELOPMENT_TEAM = AR7T5G5Z83; 330 | GENERATE_INFOPLIST_FILE = YES; 331 | INFOPLIST_FILE = ViewKitSample/Info.plist; 332 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 333 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 334 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 335 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 336 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 337 | LD_RUNPATH_SEARCH_PATHS = ( 338 | "$(inherited)", 339 | "@executable_path/Frameworks", 340 | ); 341 | MARKETING_VERSION = 1.0; 342 | PRODUCT_BUNDLE_IDENTIFIER = lucaswkuipers.ViewKitSample; 343 | PRODUCT_NAME = "$(TARGET_NAME)"; 344 | SWIFT_EMIT_LOC_STRINGS = YES; 345 | SWIFT_VERSION = 5.0; 346 | TARGETED_DEVICE_FAMILY = "1,2"; 347 | }; 348 | name = Release; 349 | }; 350 | /* End XCBuildConfiguration section */ 351 | 352 | /* Begin XCConfigurationList section */ 353 | A068A81A29B1FD91003BFD66 /* Build configuration list for PBXProject "ViewKitSample" */ = { 354 | isa = XCConfigurationList; 355 | buildConfigurations = ( 356 | A068A83129B1FD92003BFD66 /* Debug */, 357 | A068A83229B1FD92003BFD66 /* Release */, 358 | ); 359 | defaultConfigurationIsVisible = 0; 360 | defaultConfigurationName = Release; 361 | }; 362 | A068A83329B1FD92003BFD66 /* Build configuration list for PBXNativeTarget "ViewKitSample" */ = { 363 | isa = XCConfigurationList; 364 | buildConfigurations = ( 365 | A068A83429B1FD92003BFD66 /* Debug */, 366 | A068A83529B1FD92003BFD66 /* Release */, 367 | ); 368 | defaultConfigurationIsVisible = 0; 369 | defaultConfigurationName = Release; 370 | }; 371 | /* End XCConfigurationList section */ 372 | 373 | /* Begin XCSwiftPackageProductDependency section */ 374 | A068A83929B1FEEB003BFD66 /* ViewKit */ = { 375 | isa = XCSwiftPackageProductDependency; 376 | productName = ViewKit; 377 | }; 378 | /* End XCSwiftPackageProductDependency section */ 379 | }; 380 | rootObject = A068A81729B1FD91003BFD66 /* Project object */; 381 | } 382 | -------------------------------------------------------------------------------- /Sources/ViewKit/Builders/ConstraintBuilder.swift: -------------------------------------------------------------------------------- 1 | public class ConstraintBuilder { 2 | let view: UIView 3 | var constraints: [NSLayoutConstraint] = [] 4 | 5 | public init(view: UIView) { 6 | self.view = view 7 | self.view.translatesAutoresizingMaskIntoConstraints = false 8 | } 9 | 10 | @discardableResult 11 | public func activate() -> UIView { 12 | NSLayoutConstraint.activate(constraints) 13 | return view 14 | } 15 | } 16 | 17 | public extension UIView { 18 | var constraint: ConstraintBuilder { 19 | return ConstraintBuilder(view: self) 20 | } 21 | } 22 | 23 | public extension UIView { 24 | @discardableResult 25 | func addingSubviews(@ProgrammaticViewBuilder _ builder: () -> [UIView]) -> Self { 26 | for view in builder() { 27 | addSubview(view) 28 | } 29 | return self 30 | } 31 | } 32 | 33 | // MARK: - No Storing 34 | 35 | public extension ConstraintBuilder { 36 | // MARK: - Equal 37 | 38 | @discardableResult 39 | func leadingAnchor(equalTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 40 | let constraint = view.leadingAnchor.constraint(equalTo: anchor, constant: constant) 41 | constraints.append(constraint) 42 | return self 43 | } 44 | 45 | @discardableResult 46 | func leftAnchor(equalTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 47 | let constraint = view.leftAnchor.constraint(equalTo: anchor, constant: constant) 48 | constraints.append(constraint) 49 | return self 50 | } 51 | 52 | @discardableResult 53 | func trailingAnchor(equalTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 54 | let constraint = view.trailingAnchor.constraint(equalTo: anchor, constant: constant) 55 | constraints.append(constraint) 56 | return self 57 | } 58 | 59 | @discardableResult 60 | func rightAnchor(equalTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 61 | let constraint = view.rightAnchor.constraint(equalTo: anchor, constant: constant) 62 | constraints.append(constraint) 63 | return self 64 | } 65 | 66 | @discardableResult 67 | func topAnchor(equalTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 68 | let constraint = view.topAnchor.constraint(equalTo: anchor, constant: constant) 69 | constraints.append(constraint) 70 | return self 71 | } 72 | 73 | @discardableResult 74 | func bottomAnchor(equalTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 75 | let constraint = view.bottomAnchor.constraint(equalTo: anchor, constant: constant) 76 | constraints.append(constraint) 77 | return self 78 | } 79 | 80 | @discardableResult 81 | func centerXAnchor(equalTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 82 | let constraint = view.centerXAnchor.constraint(equalTo: anchor, constant: constant) 83 | constraints.append(constraint) 84 | return self 85 | } 86 | 87 | @discardableResult 88 | func centerYAnchor(equalTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 89 | let constraint = view.centerYAnchor.constraint(equalTo: anchor, constant: constant) 90 | constraints.append(constraint) 91 | return self 92 | } 93 | 94 | @discardableResult 95 | func widthAnchor(equalToConstant c: CGFloat) -> Self { 96 | let constraint = view.widthAnchor.constraint(equalToConstant: c) 97 | constraints.append(constraint) 98 | return self 99 | } 100 | 101 | @discardableResult 102 | func heightAnchor(equalToConstant c: CGFloat) -> Self { 103 | let constraint = view.heightAnchor.constraint(equalToConstant: c) 104 | constraints.append(constraint) 105 | return self 106 | } 107 | 108 | @discardableResult 109 | func heightAnchor(equalTo anchor: NSLayoutDimension, multiplier: CGFloat = 1, constant: CGFloat = 0) -> Self { 110 | let constraint = view.heightAnchor.constraint(equalTo: anchor, multiplier: multiplier, constant: constant) 111 | constraints.append(constraint) 112 | return self 113 | } 114 | 115 | @discardableResult 116 | func widthAnchor(equalTo anchor: NSLayoutDimension, multiplier: CGFloat = 1, constant: CGFloat = 0) -> Self { 117 | let constraint = view.widthAnchor.constraint(equalTo: anchor, multiplier: multiplier, constant: constant) 118 | constraints.append(constraint) 119 | return self 120 | } 121 | 122 | @discardableResult 123 | func firstBaselineAnchor(equalTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 124 | let constraint = view.firstBaselineAnchor.constraint(equalTo: anchor, constant: constant) 125 | constraints.append(constraint) 126 | return self 127 | } 128 | 129 | @discardableResult 130 | func lastBaselineAnchor(equalTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 131 | let constraint = view.lastBaselineAnchor.constraint(equalTo: anchor, constant: constant) 132 | constraints.append(constraint) 133 | return self 134 | } 135 | 136 | // MARK: - Greater 137 | 138 | @discardableResult 139 | func leadingAnchor(greaterThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 140 | let constraint = view.leadingAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 141 | constraints.append(constraint) 142 | return self 143 | } 144 | 145 | @discardableResult 146 | func leftAnchor(greaterThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 147 | let constraint = view.leftAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 148 | constraints.append(constraint) 149 | return self 150 | } 151 | 152 | @discardableResult 153 | func trailingAnchor(greaterThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 154 | let constraint = view.trailingAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 155 | constraints.append(constraint) 156 | return self 157 | } 158 | 159 | @discardableResult 160 | func rightAnchor(greaterThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 161 | let constraint = view.rightAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 162 | constraints.append(constraint) 163 | return self 164 | } 165 | 166 | @discardableResult 167 | func topAnchor(greaterThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 168 | let constraint = view.topAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 169 | constraints.append(constraint) 170 | return self 171 | } 172 | 173 | @discardableResult 174 | func bottomAnchor(greaterThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 175 | let constraint = view.bottomAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 176 | constraints.append(constraint) 177 | return self 178 | } 179 | 180 | @discardableResult 181 | func centerXAnchor(greaterThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 182 | let constraint = view.centerXAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 183 | constraints.append(constraint) 184 | return self 185 | } 186 | 187 | @discardableResult 188 | func centerYAnchor(greaterThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 189 | let constraint = view.centerYAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 190 | constraints.append(constraint) 191 | return self 192 | } 193 | 194 | @discardableResult 195 | func widthAnchor(greaterThanOrEqualToConstant c: CGFloat) -> Self { 196 | let constraint = view.widthAnchor.constraint(greaterThanOrEqualToConstant: c) 197 | constraints.append(constraint) 198 | return self 199 | } 200 | 201 | @discardableResult 202 | func widthAnchor(greaterThanOrEqualTo anchor: NSLayoutDimension, multiplier: CGFloat = 1, constant: CGFloat = 0) -> Self { 203 | let constraint = view.widthAnchor.constraint(greaterThanOrEqualTo: anchor, multiplier: multiplier, constant: constant) 204 | constraints.append(constraint) 205 | return self 206 | } 207 | 208 | @discardableResult 209 | func heightAnchor(greaterThanOrEqualToConstant c: CGFloat) -> Self { 210 | let constraint = view.heightAnchor.constraint(greaterThanOrEqualToConstant: c) 211 | constraints.append(constraint) 212 | return self 213 | } 214 | 215 | @discardableResult 216 | func heightAnchor(greaterThanOrEqualTo anchor: NSLayoutDimension, multiplier: CGFloat = 1, constant: CGFloat = 0) -> Self { 217 | let constraint = view.heightAnchor.constraint(greaterThanOrEqualTo: anchor, multiplier: multiplier, constant: constant) 218 | constraints.append(constraint) 219 | return self 220 | } 221 | 222 | @discardableResult 223 | func firstBaselineAnchor(greaterThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 224 | let constraint = view.firstBaselineAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 225 | constraints.append(constraint) 226 | return self 227 | } 228 | 229 | @discardableResult 230 | func lastBaselineAnchor(greaterThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 231 | let constraint = view.lastBaselineAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 232 | constraints.append(constraint) 233 | return self 234 | } 235 | 236 | // MARK: - Less 237 | 238 | @discardableResult 239 | func leadingAnchor(lessThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 240 | let constraint = view.leadingAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 241 | constraints.append(constraint) 242 | return self 243 | } 244 | 245 | @discardableResult 246 | func leftAnchor(lessThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 247 | let constraint = view.leftAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 248 | constraints.append(constraint) 249 | return self 250 | } 251 | 252 | @discardableResult 253 | func trailingAnchor(lessThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 254 | let constraint = view.trailingAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 255 | constraints.append(constraint) 256 | return self 257 | } 258 | 259 | 260 | @discardableResult 261 | func rightAnchor(lessThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 262 | let constraint = view.rightAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 263 | constraints.append(constraint) 264 | return self 265 | } 266 | 267 | @discardableResult 268 | func topAnchor(lessThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 269 | let constraint = view.topAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 270 | constraints.append(constraint) 271 | return self 272 | } 273 | 274 | @discardableResult 275 | func bottomAnchor(lessThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 276 | let constraint = view.bottomAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 277 | constraints.append(constraint) 278 | return self 279 | } 280 | 281 | @discardableResult 282 | func centerXAnchor(lessThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0) -> Self { 283 | let constraint = view.centerXAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 284 | constraints.append(constraint) 285 | return self 286 | } 287 | 288 | @discardableResult 289 | func centerYAnchor(lessThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 290 | let constraint = view.centerYAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 291 | constraints.append(constraint) 292 | return self 293 | } 294 | 295 | @discardableResult 296 | func widthAnchor(lessThanOrEqualToConstant c: CGFloat) -> Self { 297 | let constraint = view.widthAnchor.constraint(lessThanOrEqualToConstant: c) 298 | constraints.append(constraint) 299 | return self 300 | } 301 | 302 | @discardableResult 303 | func widthAnchor(lessThanOrEqualTo anchor: NSLayoutDimension, multiplier: CGFloat = 1, constant: CGFloat = 0) -> Self { 304 | let constraint = view.widthAnchor.constraint(lessThanOrEqualTo: anchor, multiplier: multiplier, constant: constant) 305 | constraints.append(constraint) 306 | return self 307 | } 308 | 309 | @discardableResult 310 | func heightAnchor(lessThanOrEqualToConstant c: CGFloat) -> Self { 311 | let constraint = view.heightAnchor.constraint(lessThanOrEqualToConstant: c) 312 | constraints.append(constraint) 313 | return self 314 | } 315 | 316 | @discardableResult 317 | func heightAnchor(lessThanOrEqualTo anchor: NSLayoutDimension, multiplier: CGFloat = 1, constant: CGFloat = 0) -> Self { 318 | let constraint = view.heightAnchor.constraint(lessThanOrEqualTo: anchor, multiplier: multiplier, constant: constant) 319 | constraints.append(constraint) 320 | return self 321 | } 322 | 323 | @discardableResult 324 | func firstBaselineAnchor(lessThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 325 | let constraint = view.firstBaselineAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 326 | constraints.append(constraint) 327 | return self 328 | } 329 | 330 | @discardableResult 331 | func lastBaselineAnchor(lessThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0) -> Self { 332 | let constraint = view.lastBaselineAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 333 | constraints.append(constraint) 334 | return self 335 | } 336 | } 337 | 338 | // MARK: - Storing 339 | 340 | public extension ConstraintBuilder { 341 | // MARK: - Equal 342 | 343 | @discardableResult 344 | func leadingAnchor(equalTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 345 | let constraint = view.leadingAnchor.constraint(equalTo: anchor, constant: constant) 346 | storedConstraint = constraint 347 | constraints.append(constraint) 348 | return self 349 | } 350 | 351 | @discardableResult 352 | func leftAnchor(equalTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 353 | let constraint = view.leftAnchor.constraint(equalTo: anchor, constant: constant) 354 | storedConstraint = constraint 355 | constraints.append(constraint) 356 | return self 357 | } 358 | 359 | @discardableResult 360 | func trailingAnchor(equalTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 361 | let constraint = view.trailingAnchor.constraint(equalTo: anchor, constant: constant) 362 | storedConstraint = constraint 363 | constraints.append(constraint) 364 | return self 365 | } 366 | 367 | @discardableResult 368 | func rightAnchor(equalTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 369 | let constraint = view.rightAnchor.constraint(equalTo: anchor, constant: constant) 370 | storedConstraint = constraint 371 | constraints.append(constraint) 372 | return self 373 | } 374 | 375 | @discardableResult 376 | func topAnchor(equalTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 377 | let constraint = view.topAnchor.constraint(equalTo: anchor, constant: constant) 378 | storedConstraint = constraint 379 | constraints.append(constraint) 380 | return self 381 | } 382 | 383 | @discardableResult 384 | func bottomAnchor(equalTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 385 | let constraint = view.bottomAnchor.constraint(equalTo: anchor, constant: constant) 386 | storedConstraint = constraint 387 | constraints.append(constraint) 388 | return self 389 | } 390 | 391 | @discardableResult 392 | func centerXAnchor(equalTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 393 | let constraint = view.centerXAnchor.constraint(equalTo: anchor, constant: constant) 394 | storedConstraint = constraint 395 | constraints.append(constraint) 396 | return self 397 | } 398 | 399 | @discardableResult 400 | func centerYAnchor(equalTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 401 | let constraint = view.centerYAnchor.constraint(equalTo: anchor, constant: constant) 402 | storedConstraint = constraint 403 | constraints.append(constraint) 404 | return self 405 | } 406 | 407 | @discardableResult 408 | func widthAnchor(equalToConstant c: CGFloat, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 409 | let constraint = view.widthAnchor.constraint(equalToConstant: c) 410 | storedConstraint = constraint 411 | constraints.append(constraint) 412 | return self 413 | } 414 | 415 | @discardableResult 416 | func heightAnchor(equalToConstant c: CGFloat, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 417 | let constraint = view.heightAnchor.constraint(equalToConstant: c) 418 | storedConstraint = constraint 419 | constraints.append(constraint) 420 | return self 421 | } 422 | 423 | @discardableResult 424 | func heightAnchor(equalTo anchor: NSLayoutDimension, multiplier: CGFloat = 1, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 425 | let constraint = view.heightAnchor.constraint(equalTo: anchor, multiplier: multiplier, constant: constant) 426 | storedConstraint = constraint 427 | constraints.append(constraint) 428 | return self 429 | } 430 | 431 | @discardableResult 432 | func widthAnchor(equalTo anchor: NSLayoutDimension, multiplier: CGFloat = 1, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 433 | let constraint = view.widthAnchor.constraint(equalTo: anchor, multiplier: multiplier, constant: constant) 434 | storedConstraint = constraint 435 | constraints.append(constraint) 436 | return self 437 | } 438 | 439 | @discardableResult 440 | func firstBaselineAnchor(equalTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 441 | let constraint = view.firstBaselineAnchor.constraint(equalTo: anchor, constant: constant) 442 | storedConstraint = constraint 443 | constraints.append(constraint) 444 | return self 445 | } 446 | 447 | @discardableResult 448 | func lastBaselineAnchor(equalTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 449 | let constraint = view.lastBaselineAnchor.constraint(equalTo: anchor, constant: constant) 450 | storedConstraint = constraint 451 | constraints.append(constraint) 452 | return self 453 | } 454 | 455 | // MARK: - Greater 456 | 457 | @discardableResult 458 | func leadingAnchor(greaterThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 459 | let constraint = view.leadingAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 460 | storedConstraint = constraint 461 | constraints.append(constraint) 462 | return self 463 | } 464 | 465 | @discardableResult 466 | func leftAnchor(greaterThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 467 | let constraint = view.leftAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 468 | storedConstraint = constraint 469 | constraints.append(constraint) 470 | return self 471 | } 472 | 473 | @discardableResult 474 | func trailingAnchor(greaterThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 475 | let constraint = view.trailingAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 476 | storedConstraint = constraint 477 | constraints.append(constraint) 478 | return self 479 | } 480 | 481 | @discardableResult 482 | func rightAnchor(greaterThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 483 | let constraint = view.rightAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 484 | storedConstraint = constraint 485 | constraints.append(constraint) 486 | return self 487 | } 488 | 489 | @discardableResult 490 | func topAnchor(greaterThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 491 | let constraint = view.topAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 492 | storedConstraint = constraint 493 | constraints.append(constraint) 494 | return self 495 | } 496 | 497 | @discardableResult 498 | func bottomAnchor(greaterThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 499 | let constraint = view.bottomAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 500 | storedConstraint = constraint 501 | constraints.append(constraint) 502 | return self 503 | } 504 | 505 | @discardableResult 506 | func centerXAnchor(greaterThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 507 | let constraint = view.centerXAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 508 | storedConstraint = constraint 509 | constraints.append(constraint) 510 | return self 511 | } 512 | 513 | @discardableResult 514 | func centerYAnchor(greaterThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 515 | let constraint = view.centerYAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 516 | storedConstraint = constraint 517 | constraints.append(constraint) 518 | return self 519 | } 520 | 521 | @discardableResult 522 | func widthAnchor(greaterThanOrEqualToConstant c: CGFloat, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 523 | let constraint = view.widthAnchor.constraint(greaterThanOrEqualToConstant: c) 524 | storedConstraint = constraint 525 | constraints.append(constraint) 526 | return self 527 | } 528 | 529 | @discardableResult 530 | func widthAnchor(greaterThanOrEqualTo anchor: NSLayoutDimension, multiplier: CGFloat = 1, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 531 | let constraint = view.widthAnchor.constraint(greaterThanOrEqualTo: anchor, multiplier: multiplier, constant: constant) 532 | storedConstraint = constraint 533 | constraints.append(constraint) 534 | return self 535 | } 536 | 537 | @discardableResult 538 | func heightAnchor(greaterThanOrEqualToConstant c: CGFloat, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 539 | let constraint = view.heightAnchor.constraint(greaterThanOrEqualToConstant: c) 540 | storedConstraint = constraint 541 | constraints.append(constraint) 542 | return self 543 | } 544 | 545 | @discardableResult 546 | func heightAnchor(greaterThanOrEqualTo anchor: NSLayoutDimension, multiplier: CGFloat = 1, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 547 | let constraint = view.heightAnchor.constraint(greaterThanOrEqualTo: anchor, multiplier: multiplier, constant: constant) 548 | storedConstraint = constraint 549 | constraints.append(constraint) 550 | return self 551 | } 552 | 553 | @discardableResult 554 | func firstBaselineAnchor(greaterThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 555 | let constraint = view.firstBaselineAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 556 | storedConstraint = constraint 557 | constraints.append(constraint) 558 | return self 559 | } 560 | 561 | @discardableResult 562 | func lastBaselineAnchor(greaterThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 563 | let constraint = view.lastBaselineAnchor.constraint(greaterThanOrEqualTo: anchor, constant: constant) 564 | storedConstraint = constraint 565 | constraints.append(constraint) 566 | return self 567 | } 568 | 569 | // MARK: - Less 570 | 571 | @discardableResult 572 | func leadingAnchor(lessThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 573 | let constraint = view.leadingAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 574 | storedConstraint = constraint 575 | constraints.append(constraint) 576 | return self 577 | } 578 | 579 | @discardableResult 580 | func leftAnchor(lessThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 581 | let constraint = view.leftAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 582 | storedConstraint = constraint 583 | constraints.append(constraint) 584 | return self 585 | } 586 | 587 | @discardableResult 588 | func trailingAnchor(lessThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 589 | let constraint = view.trailingAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 590 | storedConstraint = constraint 591 | constraints.append(constraint) 592 | return self 593 | } 594 | 595 | @discardableResult 596 | func rightAnchor(lessThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 597 | let constraint = view.rightAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 598 | storedConstraint = constraint 599 | constraints.append(constraint) 600 | return self 601 | } 602 | 603 | @discardableResult 604 | func topAnchor(lessThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 605 | let constraint = view.topAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 606 | storedConstraint = constraint 607 | constraints.append(constraint) 608 | return self 609 | } 610 | 611 | @discardableResult 612 | func bottomAnchor(lessThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 613 | let constraint = view.bottomAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 614 | storedConstraint = constraint 615 | constraints.append(constraint) 616 | return self 617 | } 618 | 619 | @discardableResult 620 | func centerXAnchor(lessThanOrEqualTo anchor: NSLayoutXAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 621 | let constraint = view.centerXAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 622 | storedConstraint = constraint 623 | constraints.append(constraint) 624 | return self 625 | } 626 | 627 | @discardableResult 628 | func centerYAnchor(lessThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 629 | let constraint = view.centerYAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 630 | storedConstraint = constraint 631 | constraints.append(constraint) 632 | return self 633 | } 634 | 635 | @discardableResult 636 | func widthAnchor(lessThanOrEqualToConstant c: CGFloat, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 637 | let constraint = view.widthAnchor.constraint(lessThanOrEqualToConstant: c) 638 | storedConstraint = constraint 639 | constraints.append(constraint) 640 | return self 641 | } 642 | 643 | @discardableResult 644 | func widthAnchor(lessThanOrEqualTo anchor: NSLayoutDimension, multiplier: CGFloat = 1, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 645 | let constraint = view.widthAnchor.constraint(lessThanOrEqualTo: anchor, multiplier: multiplier, constant: constant) 646 | storedConstraint = constraint 647 | constraints.append(constraint) 648 | return self 649 | } 650 | 651 | @discardableResult 652 | func heightAnchor(lessThanOrEqualToConstant c: CGFloat, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 653 | let constraint = view.heightAnchor.constraint(lessThanOrEqualToConstant: c) 654 | storedConstraint = constraint 655 | constraints.append(constraint) 656 | return self 657 | } 658 | 659 | @discardableResult 660 | func heightAnchor(lessThanOrEqualTo anchor: NSLayoutDimension, multiplier: CGFloat = 1, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 661 | let constraint = view.heightAnchor.constraint(lessThanOrEqualTo: anchor, multiplier: multiplier, constant: constant) 662 | storedConstraint = constraint 663 | constraints.append(constraint) 664 | return self 665 | } 666 | 667 | @discardableResult 668 | func firstBaselineAnchor(lessThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 669 | let constraint = view.firstBaselineAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 670 | storedConstraint = constraint 671 | constraints.append(constraint) 672 | return self 673 | } 674 | 675 | @discardableResult 676 | func lastBaselineAnchor(lessThanOrEqualTo anchor: NSLayoutYAxisAnchor, constant: CGFloat = 0, storedIn storedConstraint: inout NSLayoutConstraint) -> Self { 677 | let constraint = view.lastBaselineAnchor.constraint(lessThanOrEqualTo: anchor, constant: constant) 678 | storedConstraint = constraint 679 | constraints.append(constraint) 680 | return self 681 | } 682 | 683 | } 684 | --------------------------------------------------------------------------------