├── .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 |
--------------------------------------------------------------------------------