├── .gitignore ├── CHANGELOG.md ├── Documentation └── Declarative_UIKit_with_10_lines_of_code_SwiftUI_Xcode_Preview.png ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Withable ├── UI │ ├── UILabel+Extensions.swift │ ├── UIImageView+Extensions.swift │ ├── UIStackView+Extensions.swift │ ├── UIButton+Extensions.swift │ └── UIView+Extensions.swift ├── NSObject+Extensions.swift └── Withable.swift ├── Package.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | 4 | * 0.0.10 5 | 6 | + Visibility 7 | 8 | * 0.0.0 - 0.0.9 9 | 10 | + Initial files 11 | + Documentation 12 | -------------------------------------------------------------------------------- /Documentation/Declarative_UIKit_with_10_lines_of_code_SwiftUI_Xcode_Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Geri-Borbas/iOS.Package.Withable/HEAD/Documentation/Declarative_UIKit_with_10_lines_of_code_SwiftUI_Xcode_Preview.png -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Withable/UI/UILabel+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILabel+Extensions.swift 3 | // Withable 4 | // 5 | // Created by Geri Borbás on 08/04/2022. 6 | // http://www.twitter.com/Geri_Borbas 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | public extension UILabel { 13 | 14 | func with(text: String?) -> Self { 15 | with { 16 | $0.text = text 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Withable/UI/UIImageView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+Extensions.swift 3 | // Withable 4 | // 5 | // Created by Geri Borbás on 30/03/2022. 6 | // http://www.twitter.com/Geri_Borbas 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | public extension UIImageView { 13 | 14 | func with(image: UIImage?) -> Self { 15 | with { 16 | $0.image = image 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Withable", 8 | products: [ 9 | .library( 10 | name: "Withable", 11 | targets: ["Withable"] 12 | ) 13 | ], 14 | targets: [ 15 | .target( 16 | name: "Withable", 17 | path: "Withable" 18 | ) 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Withable/UI/UIStackView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStackView+Extensions.swift 3 | // Withable 4 | // 5 | // Created by Geri Borbás on 08/04/2022. 6 | // http://www.twitter.com/Geri_Borbas 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | public extension UIStackView { 13 | 14 | func horizontal(spacing: CGFloat = 0) -> Self { 15 | with { 16 | $0.axis = .horizontal 17 | $0.spacing = spacing 18 | } 19 | } 20 | 21 | func vertical(spacing: CGFloat = 0) -> Self { 22 | with { 23 | $0.axis = .vertical 24 | $0.spacing = spacing 25 | } 26 | } 27 | 28 | func views(_ views: UIView ...) -> Self { 29 | views.forEach { self.addArrangedSubview($0) } 30 | return self 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Withable/UI/UIButton+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+Extensions.swift 3 | // Withable 4 | // 5 | // Created by Geri Borbás on 30/03/2022. 6 | // http://www.twitter.com/Geri_Borbas 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | public extension UIButton { 13 | 14 | typealias Action = () -> Void 15 | 16 | var onTap: Action? { 17 | get { 18 | associatedObject(for: "onTapAction") as? Action 19 | } 20 | set { 21 | set(associatedObject: newValue, for: "onTapAction") 22 | } 23 | } 24 | 25 | func onTap(_ action: @escaping Action) -> Self { 26 | self.onTap = action 27 | addTarget(self, action: #selector(didTouchUpInside), for: .touchUpInside) 28 | return self 29 | } 30 | 31 | @objc func didTouchUpInside() { 32 | onTap?() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Geri Borbás 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 | -------------------------------------------------------------------------------- /Withable/NSObject+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+Extensions.swift 3 | // Withable 4 | // 5 | // Created by Geri Borbás on 08/04/2022. 6 | // http://www.twitter.com/Geri_Borbas 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension NSObject { 13 | 14 | struct Keys { 15 | static var associatedObjects: UInt8 = 0 16 | } 17 | 18 | var associatedObjects: NSMutableDictionary { 19 | get { 20 | if let associatedObjects = objc_getAssociatedObject(self, &Keys.associatedObjects) as? NSMutableDictionary { 21 | return associatedObjects 22 | } else { 23 | let associatedObjects: NSMutableDictionary = [:] 24 | objc_setAssociatedObject(self, &Keys.associatedObjects, associatedObjects, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 25 | return associatedObjects 26 | } 27 | } 28 | } 29 | 30 | public func set(associatedObject: Any?, for key: AnyHashable) { 31 | if let associatedObject = associatedObject { 32 | associatedObjects[key] = associatedObject 33 | } else { 34 | remove(associatedObjectFor: key) 35 | } 36 | } 37 | 38 | public func associatedObject(for key: AnyHashable) -> Any? { 39 | associatedObjects[key] 40 | } 41 | 42 | func remove(associatedObjectFor key: AnyHashable) { 43 | associatedObjects.removeObject(forKey: key) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Withable/Withable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Withable.swift 3 | // Withable 4 | // 5 | // Created by Geri Borbás on 28/11/2020. 6 | // http://www.twitter.com/Geri_Borbas 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | // MARK: - Withable for Objects 13 | 14 | public protocol ObjectWithable: AnyObject { 15 | 16 | associatedtype T 17 | 18 | /// Provides a closure to configure instances inline. 19 | /// - Parameter closure: A closure `self` as the argument. 20 | /// - Returns: Simply returns the instance after called the `closure`. 21 | @discardableResult func with(_ closure: (_ instance: T) -> Void) -> T 22 | } 23 | 24 | public extension ObjectWithable { 25 | 26 | @discardableResult func with(_ closure: (_ instance: Self) -> Void) -> Self { 27 | closure(self) 28 | return self 29 | } 30 | } 31 | 32 | extension NSObject: ObjectWithable { } 33 | 34 | 35 | // MARK: - Withable for Values 36 | 37 | public protocol Withable { 38 | 39 | associatedtype T 40 | 41 | /// Provides a closure to configure instances inline. 42 | /// - Parameter closure: A closure with a mutable copy of `self` as the argument. 43 | /// - Returns: Simply returns the mutated copy of the instance after called the `closure`. 44 | @discardableResult func with(_ closure: (_ instance: inout T) -> Void) -> T 45 | } 46 | 47 | public extension Withable { 48 | 49 | @discardableResult func with(_ closure: (_ instance: inout Self) -> Void) -> Self { 50 | var copy = self 51 | closure(©) 52 | return copy 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Withable/UI/UIView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Extensions.swift 3 | // Withable 4 | // 5 | // Created by Geri Borbás on 08/04/2022. 6 | // http://www.twitter.com/Geri_Borbas 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | public extension UIView { 13 | 14 | static var spacer: UIView { 15 | UIView().with { 16 | $0.setContentHuggingPriority(.required, for: .horizontal) 17 | $0.setContentHuggingPriority(.required, for: .vertical) 18 | } 19 | } 20 | 21 | var withRedLines: Self { 22 | with { 23 | $0.layer.borderWidth = 1 24 | $0.layer.cornerRadius = 2 25 | $0.layer.borderColor = UIColor.red.withAlphaComponent(0.6 * 0.5).cgColor 26 | $0.backgroundColor = UIColor.red.withAlphaComponent(0.2 * 0.5) 27 | } 28 | } 29 | 30 | var inspect: Self { 31 | withRedLines 32 | } 33 | } 34 | 35 | 36 | // MARK: Constraints 37 | 38 | public extension UIView { 39 | 40 | func pin(to: UILayoutGuide, insets: UIEdgeInsets = .zero) { 41 | translatesAutoresizingMaskIntoConstraints = false 42 | topAnchor.constraint(equalTo: to.topAnchor, constant: insets.top).isActive = true 43 | bottomAnchor.constraint(equalTo: to.bottomAnchor, constant: -insets.bottom).isActive = true 44 | leftAnchor.constraint(equalTo: to.leftAnchor, constant: insets.left).isActive = true 45 | rightAnchor.constraint(equalTo: to.rightAnchor, constant: -insets.right).isActive = true 46 | } 47 | 48 | func pin(to: UIView, insets: UIEdgeInsets = .zero) { 49 | translatesAutoresizingMaskIntoConstraints = false 50 | topAnchor.constraint(equalTo: to.topAnchor, constant: insets.top).isActive = true 51 | bottomAnchor.constraint(equalTo: to.bottomAnchor, constant: -insets.bottom).isActive = true 52 | leftAnchor.constraint(equalTo: to.leftAnchor, constant: insets.left).isActive = true 53 | rightAnchor.constraint(equalTo: to.rightAnchor, constant: -insets.right).isActive = true 54 | } 55 | 56 | @discardableResult func top(to: UIView, inset: CGFloat = 0) -> NSLayoutConstraint { 57 | translatesAutoresizingMaskIntoConstraints = false 58 | return topAnchor.constraint(equalTo: to.topAnchor, constant: inset).with { 59 | $0.isActive = true 60 | } 61 | } 62 | 63 | @discardableResult func centerX(to: UIView, inset: CGFloat = 0) -> NSLayoutConstraint { 64 | translatesAutoresizingMaskIntoConstraints = false 65 | return centerXAnchor.constraint(equalTo: to.centerXAnchor, constant: inset).with { 66 | $0.isActive = true 67 | } 68 | } 69 | 70 | @discardableResult func set(height: CGFloat) -> NSLayoutConstraint { 71 | translatesAutoresizingMaskIntoConstraints = false 72 | return heightAnchor.constraint(equalToConstant: height).with { 73 | $0.isActive = true 74 | } 75 | } 76 | 77 | @discardableResult func set(width: CGFloat) -> NSLayoutConstraint { 78 | translatesAutoresizingMaskIntoConstraints = false 79 | return widthAnchor.constraint(equalToConstant: width).with { 80 | $0.isActive = true 81 | } 82 | } 83 | } 84 | 85 | 86 | // MARK: - onMoveToSuperview 87 | 88 | public extension UIView { 89 | 90 | typealias ViewAction = (_ view: UIView) -> Void 91 | typealias ViewAndSuperviewAction = (_ view: UIView, _ superview: UIView) -> Void 92 | 93 | /// The `onMoveToSuperview` closure will be called once, right after this view called its 94 | /// `didMoveToSuperView()`. Suitable place to add constraints to this view instance. 95 | /// See https://developer.apple.com/documentation/uikit/uiview/1622512-updateconstraints 96 | @discardableResult func onMoveToSuperview(_ onMoveToSuperview: @escaping ViewAction) -> Self { 97 | self.onMoveToSuperview = onMoveToSuperview 98 | return self 99 | } 100 | 101 | @discardableResult func onMoveToSuperview(_ onMoveToSuperview: @escaping ViewAndSuperviewAction) -> Self { 102 | self.onMoveToSuperview = { view in 103 | guard let superview = self.superview else { return } 104 | onMoveToSuperview(self, superview) 105 | } 106 | return self 107 | } 108 | } 109 | 110 | 111 | // MARK: - Swizzle 112 | 113 | extension UIView { 114 | 115 | static var notSwizzled = true 116 | 117 | struct Keys { 118 | static var viewAction: UInt8 = 0 119 | } 120 | 121 | /// The `onMoveToSuperview` closure will be called once, right after this view called its 122 | /// `didMoveToSuperView()`. Suitable place to add constraints to this view instance. 123 | /// See https://developer.apple.com/documentation/uikit/uiview/1622512-updateconstraints 124 | var onMoveToSuperview: ViewAction? { 125 | get { 126 | objc_getAssociatedObject(self, &Keys.viewAction) as? ViewAction 127 | } 128 | set { 129 | swizzleIfNeeded() 130 | objc_setAssociatedObject(self, &Keys.viewAction, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 131 | } 132 | } 133 | 134 | @objc func originalDidMoveToSuperview() { 135 | // Original implementation will be copied here. 136 | } 137 | 138 | @objc func swizzledDidMoveToSuperview() { 139 | originalDidMoveToSuperview() 140 | if superview != nil { 141 | onMoveToSuperview?(self) 142 | onMoveToSuperview = nil 143 | } 144 | } 145 | 146 | func swizzleIfNeeded() { 147 | 148 | guard Self.notSwizzled else { 149 | return 150 | } 151 | 152 | guard let viewClass: AnyClass = object_getClass(self) else { 153 | return print("Could not get `UIView` class.") 154 | } 155 | 156 | let selector = #selector(didMoveToSuperview) 157 | guard let method = class_getInstanceMethod(viewClass, selector) else { 158 | return print("Could not get `didMoveToSuperview()` selector.") 159 | } 160 | 161 | let originalSelector = #selector(originalDidMoveToSuperview) 162 | guard let originalMethod = class_getInstanceMethod(viewClass, originalSelector) else { 163 | return print("Could not get original `originalDidMoveToSuperview()` selector.") 164 | } 165 | 166 | let swizzledSelector = #selector(swizzledDidMoveToSuperview) 167 | guard let swizzledMethod = class_getInstanceMethod(viewClass, swizzledSelector) else { 168 | return print("Could not get swizzled `swizzledDidMoveToSuperview()` selector.") 169 | } 170 | 171 | // Swap implementations. 172 | method_exchangeImplementations(method, originalMethod) 173 | method_exchangeImplementations(method, swizzledMethod) 174 | 175 | // Flag. 176 | Self.notSwizzled = false 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Withable 2 | 3 | 📐 Declarative UIKit in 10 lines of code. 4 | 5 |

6 | 7 | See corresponding article at [**Declarative UIKit with 10 lines of code** A simple extension instead of libraries] for more. 8 | 9 | 10 | ## How to use 11 | 12 | With a **single extension** on `AnyObject` you can do things like this. 13 | 14 | ```Swift 15 | class ContentViewController: UIViewController { 16 | 17 | ... 18 | 19 | lazy var titleLabel = UILabel() 20 | .with { 21 | $0.text = viewModel.title 22 | $0.textColor = .label 23 | $0.font = .preferredFont(forTextStyle: .largeTitle) 24 | } 25 | 26 | ... 27 | } 28 | ``` 29 | 30 | With **any kind of object**, really. 31 | 32 | ```Swift 33 | lazy var submitButton = UIButton() 34 | .with { 35 | $0.setTitle("Submit", for: .normal) 36 | $0.addTarget(self, action: #selector(didTapSubmitButton), for: .touchUpInside) 37 | } 38 | ``` 39 | 40 | ```Swift 41 | present( 42 | DetailViewController() 43 | .with { 44 | $0.modalTransitionStyle = .crossDissolve 45 | $0.modalPresentationStyle = .overCurrentContext 46 | }, 47 | animated: true 48 | ) 49 | ``` 50 | 51 | ```Swift 52 | present( 53 | UIAlertController(title: title, message: message, preferredStyle: .alert) 54 | .with { 55 | $0.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) 56 | }, 57 | animated: true 58 | ) 59 | ``` 60 | 61 | ```Swift 62 | let today = DateFormatter() 63 | .with { 64 | $0.dateStyle = .medium 65 | $0.locale = Locale(identifier: "en_US") 66 | } 67 | .string(from: Date()) 68 | ``` 69 | 70 | ```Swift 71 | lazy var displayLink = CADisplayLink(target: self, selector: #selector(update)) 72 | .with { 73 | $0.isPaused = true 74 | $0.preferredFramesPerSecond = 120 75 | $0.add(to: RunLoop.main, forMode: .common) 76 | } 77 | ``` 78 | 79 | Even value types as well (after conforming to `Withable`). 80 | 81 | ```Swift 82 | extension PersonNameComponents: Withable { } 83 | 84 | let name = PersonNameComponents() 85 | .with { 86 | $0.givenName = "Geri" 87 | $0.familyName = "Borbás" 88 | } 89 | ``` 90 | 91 | Not to mention 3D stuff (`ARKit`, `RealityKit`, `SceneKit`). 92 | 93 | ```Swift 94 | view.scene.addAnchor( 95 | AnchorEntity(plane: .horizontal) 96 | .with { 97 | $0.addChild( 98 | ModelEntity( 99 | mesh: MeshResource.generateBox(size: 0.3), 100 | materials: [ 101 | SimpleMaterial(color: .green, isMetallic: true) 102 | ] 103 | ) 104 | ) 105 | } 106 | ) 107 | ``` 108 | 109 | 110 | ## How it works 111 | 112 | It is implemented in this `with` method. 💎 113 | 114 | ```Swift 115 | public extension Withable { 116 | 117 | func with(_ closure: (Self) -> Void) -> Self { 118 | closure(self) 119 | return self 120 | } 121 | } 122 | ``` 123 | 124 | The method implements pretty **classic patterns**. You can think of it as something between an unspecialized/parametric builder, or a **decorator** with customizable/pluggable decorating behaviour. See [`Withable.swift`] for all details (generics, value types). 125 | 126 | 127 | ## UIKit benefits 128 | 129 | The package contains a couple of **convinient extensions** of `UIKit` classes what I use (probably will be moved to their own package as they grow). I left them here intentionally as they may exemplify how you can **create your own extensions** tailored for your codebases' needs. 130 | 131 | For example, you may create a convenient **`text` decorator** for `UILabel`. 132 | 133 | ```Swift 134 | extension UILabel { 135 | 136 | func with(text: String?) -> Self { 137 | with { 138 | $0.text = text 139 | } 140 | } 141 | } 142 | ``` 143 | 144 | Furthermore, you can condense your **styles to simple extensions** like this. 145 | 146 | ```Swift 147 | extension UILabel { 148 | 149 | var withTitleStyle: Self { 150 | with { 151 | $0.textColor = .label 152 | $0.font = .preferredFont(forTextStyle: .largeTitle) 153 | } 154 | } 155 | 156 | var withPropertyStyle: Self { 157 | with { 158 | $0.textColor = .systemBackground 159 | $0.font = .preferredFont(forTextStyle: .headline) 160 | $0.setContentCompressionResistancePriority(.required, for: .vertical) 161 | } 162 | } 163 | 164 | var withPropertyValueStyle: Self { 165 | with { 166 | $0.textColor = .systemGray 167 | $0.font = .preferredFont(forTextStyle: .body) 168 | } 169 | } 170 | 171 | var withParagraphStyle: Self { 172 | with { 173 | $0.textColor = .label 174 | $0.numberOfLines = 0 175 | $0.font = .preferredFont(forTextStyle: .footnote) 176 | } 177 | } 178 | } 179 | ``` 180 | 181 | With extensions like that, you can clean up view controllers. 182 | 183 | ```Swift 184 | class ContentViewController: UIViewController { 185 | 186 | let viewModel = Planets().earth 187 | 188 | private lazy var body = UIStackView().vertical(spacing: 10).views( 189 | UILabel() 190 | .with(text: viewModel.title) 191 | .withTitleStyle, 192 | UIStackView().vertical(spacing: 5).views( 193 | UIStackView().horizontal(spacing: 5).views( 194 | UILabel() 195 | .with(text: "size") 196 | .withPropertyStyle 197 | .withBox, 198 | UILabel() 199 | .with(text: viewModel.properties.size) 200 | .withPropertyValueStyle, 201 | UIView.spacer 202 | ), 203 | UIStackView().horizontal(spacing: 5).views( 204 | UILabel() 205 | .with(text: "distance") 206 | .withPropertyStyle 207 | .withBox, 208 | UILabel() 209 | .with(text: viewModel.properties.distance) 210 | .withPropertyValueStyle, 211 | UIView.spacer 212 | ), 213 | UIStackView().horizontal(spacing: 5).views( 214 | UILabel() 215 | .with(text: "mass") 216 | .withPropertyStyle 217 | .withBox, 218 | UILabel() 219 | .with(text: viewModel.properties.mass) 220 | .withPropertyValueStyle, 221 | UIView.spacer 222 | ) 223 | ), 224 | UIImageView() 225 | .with(image: UIImage(named: viewModel.imageAssetName)), 226 | UILabel() 227 | .with(text: viewModel.paragraphs.first) 228 | .withParagraphStyle, 229 | UILabel() 230 | .with(text: viewModel.paragraphs.last) 231 | .withParagraphStyle, 232 | UIView.spacer 233 | ) 234 | 235 | override func viewDidLoad() { 236 | super.viewDidLoad() 237 | view.addSubview(body) 238 | view.backgroundColor = .systemBackground 239 | body.pin( 240 | to: view.safeAreaLayoutGuide, 241 | insets: UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 30) 242 | ) 243 | } 244 | } 245 | ``` 246 | 247 | I recommend to read the corresponding article at [**Declarative UIKit with 10 lines of code** A simple extension instead of libraries] to read more about the background and more examples. 248 | 249 | 250 | ## Used by Apple 251 | 252 | Later on, I found out that on occasions **Apple uses the very same pattern** to enable decorating objects inline. These decorator functions are even uses the same `with` naming convention. 253 | 254 | These examples below are in vanilla `UIKit`. 🍦 255 | 256 | ```Swift 257 | let arrow = UIImage(named: "Arrow").withTintColor(.blue) 258 | let mail = UIImage(systemName: "envelope").withRenderingMode(.alwaysTemplate) 259 | let color = UIColor.label.withAlphaComponent(0.5) 260 | ``` 261 | 262 | * [`UIImage.withTintColor(_:)`] 263 | * [`UIImage.withAlphaComponent(_:)`] 264 | * [`UIImage.Configuration.withTraitCollection(_:)`] 265 | * More examples in [`UIImage.Configuration`] 266 | 267 | 268 | ## Stored properties in extensions 269 | 270 | In addition, the package contains an `NSObject` extension that helps creating **stored properties in extensions**. I ended up including it because I found extending `UIKit` classes with stored properties is a pretty common usecase. See [`NSObject+Extensions.swift`] and [`UIButton+Extensions.swift`] for more. 271 | 272 | You can do things like this. 273 | 274 | ```Swift 275 | extension UITextField { 276 | 277 | var nextTextField: UITextField? { 278 | get { 279 | associatedObject(for: "nextTextField") as? UITextField 280 | } 281 | set { 282 | set(associatedObject: newValue, for: "nextTextField") 283 | } 284 | } 285 | } 286 | ``` 287 | 288 | 289 | ## Declare constraints inline 290 | 291 | One more secret weapon is the [`UIView.onMoveToSuperview`] extension, which is simply a closure called (once) when the `view` gets added to a `superview`. With that, you can declare the constraints in advance using this closure at initialization time, then they are added/activated later on at runtime by the time when the view has a superview. See [Keyboard Avoidance] repository for usage examples. 292 | 293 | 294 | ## License 295 | 296 | > Licensed under the [**MIT License**](https://en.wikipedia.org/wiki/MIT_License). 297 | 298 | 299 | [`Withable.swift`]: Withable/Withable.swift 300 | [**Declarative UIKit with 10 lines of code** A simple extension instead of libraries]: https://blog.eppz.eu/declarative-uikit-with-10-lines-of-code/ 301 | [`NSObject+Extensions.swift`]: Withable/NSObject+Extensions.swift 302 | [`UIButton+Extensions.swift`]: Withable/UI/UIButton+Extensions.swift 303 | [`UIImage.withTintColor(_:)`]: https://developer.apple.com/documentation/uikit/uiimage/3327300-withtintcolor 304 | [`UIImage.withAlphaComponent(_:)`]: https://developer.apple.com/documentation/uikit/uicolor/1621922-withalphacomponent 305 | [`UIImage.Configuration.withTraitCollection(_:)`]: https://developer.apple.com/documentation/uikit/uiimage/configuration/3295946-withtraitcollection 306 | [`UIImage.Configuration`]: https://developer.apple.com/documentation/uikit/uiimage/configuration 307 | [`UIView.onMoveToSuperview`]: Withable/UIView+Extensions.swift 308 | [Keyboard Avoidance]: https://github.com/Geri-Borbas/iOS.Blog.Keyboard_Avoidance 309 | --------------------------------------------------------------------------------