Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d5067212a | |||
|
|
72a5b59c44 | ||
|
|
d678965154 | ||
|
|
0cceda1ad9 | ||
|
|
2cd52d3d12 | ||
|
|
1743bc2d24 | ||
|
|
872d70dc21 | ||
|
|
6eb57f5c44 | ||
|
|
ac9d010b8e | ||
|
|
255e5cfe1a | ||
|
|
f57dc50975 | ||
|
|
971278fa3c | ||
|
|
ec3442ed1c | ||
|
|
4f6a83556d | ||
|
|
d16a42fddc | ||
|
|
c459c9eaf6 | ||
|
|
1d2c595c78 | ||
|
|
9f1db7c121 | ||
|
|
705639b2f5 |
@@ -1,5 +1,8 @@
|
|||||||
# `API` — networking for LCEssentials
|
# `API` — networking for LCEssentials
|
||||||
|
|
||||||
|
Part of the reference set: [Extensions.md](Extensions.md) ·
|
||||||
|
[SwiftUI.md](SwiftUI.md) · [UIKit.md](UIKit.md).
|
||||||
|
|
||||||
`API` is an `actor` that wraps `URLSession` for JSON REST calls and multipart
|
`API` is an `actor` that wraps `URLSession` for JSON REST calls and multipart
|
||||||
uploads. One line to send a typed request, decode the response, and get a
|
uploads. One line to send a typed request, decode the response, and get a
|
||||||
consistent error — instead of re-writing the same `URLRequest` / status-code /
|
consistent error — instead of re-writing the same `URLRequest` / status-code /
|
||||||
|
|||||||
2018
Documentation/Extensions.md
Normal file
2018
Documentation/Extensions.md
Normal file
File diff suppressed because it is too large
Load Diff
117
Documentation/SwiftUI.md
Normal file
117
Documentation/SwiftUI.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# LCEssentials — SwiftUI
|
||||||
|
|
||||||
|
SwiftUI components and `View` helpers. Foundation/value-type helpers are in
|
||||||
|
[Extensions.md](Extensions.md); UIKit-era helpers in [UIKit.md](UIKit.md).
|
||||||
|
|
||||||
|
Every section is a collapsible block — click a heading to expand it.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Navigation](#navigation)
|
||||||
|
- [View helpers](#view-helpers)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>LCENavigationView</b> — customizable navigation bar (iOS 15+)</summary>
|
||||||
|
|
||||||
|
A drop-in replacement for the system navigation bar with left/right buttons,
|
||||||
|
title + subtitle, background colour, and a hide toggle. Configuration methods
|
||||||
|
return `self`, so they chain. **Per the workspace iOS standards, this component
|
||||||
|
is mandatory on new SwiftUI screens instead of a hand-rolled bar.**
|
||||||
|
|
||||||
|
`@available(iOS 15, *)`, iOS only.
|
||||||
|
|
||||||
|
### `init(title: (any View) = Text(""), subTitle: (any View) = Text(""), @ViewBuilder content: () -> Content)`
|
||||||
|
|
||||||
|
The `content` closure is everything shown **below** the bar.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
LCENavigationView(title: Text("Profile")) {
|
||||||
|
ScrollView {
|
||||||
|
ProfileForm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func setTitle(text: (any View) = Text(""), subTitle: (any View)? = nil) -> LCENavigationView`
|
||||||
|
|
||||||
|
Set (or replace) the title and optional subtitle. Passing no `subTitle` hides
|
||||||
|
the subtitle row.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
LCENavigationView { Content() }
|
||||||
|
.setTitle(text: Text("Orders"), subTitle: Text("32 open"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func setLeftButton(text: Text = Text(""), image: (any View)? = nil, action: @escaping () -> Void) -> LCENavigationView`
|
||||||
|
|
||||||
|
Configure the leading button. If the trailing button has no text/image yet, a
|
||||||
|
transparent placeholder is added on that side so the title stays centred.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
.setLeftButton(text: Text("Back"), image: Image(systemName: "chevron.left")) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func setRightButton(text: Text = Text(""), image: (any View)? = nil, action: @escaping () -> Void) -> LCENavigationView`
|
||||||
|
|
||||||
|
Configure the trailing button (same placeholder behaviour for the leading side).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
.setRightButton(image: Image(systemName: "plus")) {
|
||||||
|
showingNewItem = true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func hideNavigationView(_ hide: Bool) -> LCENavigationView`
|
||||||
|
|
||||||
|
Show or hide the whole bar (the content stays).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
.hideNavigationView(isFullScreenMedia)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func setNavigationBarBackgroundColor(_ color: Color) -> LCENavigationView`
|
||||||
|
|
||||||
|
Bar background colour (extends into the top safe area). Defaults to `.clear`.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
.setNavigationBarBackgroundColor(.blue.opacity(0.1))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full example
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct OrdersScreen: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@State private var showingNew = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
LCENavigationView(title: Text("Orders")) {
|
||||||
|
OrdersList()
|
||||||
|
}
|
||||||
|
.setLeftButton(text: Text("Back"),
|
||||||
|
image: Image(systemName: "chevron.left")) { dismiss() }
|
||||||
|
.setRightButton(image: Image(systemName: "plus")) { showingNew = true }
|
||||||
|
.setNavigationBarBackgroundColor(Color(.systemBackground))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> `LCENavigationState` (the `@Published` backing store) and the reflection-based
|
||||||
|
> `Text.string` / tag helpers in `View+Ext.swift` are `internal` implementation
|
||||||
|
> details — not part of the public API.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## View helpers
|
||||||
|
|
||||||
|
There are currently no public standalone `View` extensions — the `getTag` /
|
||||||
|
`extractTag` reflection utilities in `SwiftUI/View+Ext.swift` are `internal` and
|
||||||
|
exist only to support `LCENavigationView`'s subtitle handling.
|
||||||
815
Documentation/UIKit.md
Normal file
815
Documentation/UIKit.md
Normal file
@@ -0,0 +1,815 @@
|
|||||||
|
# LCEssentials — UIKit
|
||||||
|
|
||||||
|
UIKit-era helpers: programmatic layout & constraints, view/control extensions,
|
||||||
|
navigation, table/collection helpers, and drop-in components (image picker, image
|
||||||
|
zoom, snackbar, GIF loading).
|
||||||
|
|
||||||
|
Foundation/value-type helpers are in [Extensions.md](Extensions.md); SwiftUI
|
||||||
|
helpers in [SwiftUI.md](SwiftUI.md).
|
||||||
|
|
||||||
|
Every section is a collapsible block — click a heading to expand it.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Layout & Constraints](#layout--constraints)
|
||||||
|
- [Views & Controls](#views--controls)
|
||||||
|
- [Navigation & Controllers](#navigation--controllers)
|
||||||
|
- [Collections & Tables](#collections--tables)
|
||||||
|
- [Media & Components](#media--components)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layout & Constraints
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UIView</b> — programmatic constraints, chainable anchors</summary>
|
||||||
|
|
||||||
|
### `enum AnchorType`
|
||||||
|
The anchor to pin when using `setConstraintsTo`. Cases: `all`, `top`, `bottom`,
|
||||||
|
`leading`, `trailing`, `left`, `right`, `centerX`, `centerY`, `width`, `heigth`
|
||||||
|
*(sic)*, `topToBottom`, `bottomToTop`, `leadingToTrailing`, `trailingToLeading`,
|
||||||
|
plus `…GreaterThanOrEqualTo` / `…LessThanOrEqualTo` variants.
|
||||||
|
|
||||||
|
### `@discardableResult func setConstraintsTo(parentView: UIView, anchorType: AnchorType, value: CGFloat, safeArea: Bool = false) -> Self`
|
||||||
|
Activate one constraint against `parentView`. Remembers `parentView` (in
|
||||||
|
`viewReference`) so follow-up calls can omit it. `safeArea: true` pins top/bottom
|
||||||
|
to the safe-area guide (iOS 11+).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
card.addSubview(label)
|
||||||
|
label.setConstraintsTo(parentView: card, anchorType: .top, value: 12, safeArea: false)
|
||||||
|
.setConstraints(.leading, 16)
|
||||||
|
.setConstraints(.trailing, -16)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `@discardableResult func setConstraintsTo(_ parentView:_ anchorType:_ value:_ safeArea: Bool = false) -> Self`
|
||||||
|
Positional-argument shorthand for the above.
|
||||||
|
|
||||||
|
### `@discardableResult func setConstraintsTo(anchorType: AnchorType, value: CGFloat, safeArea: Bool = false) -> UIView`
|
||||||
|
Reuse the last `parentView` (`viewReference`). **Traps** with `fatalError` if no
|
||||||
|
`parentView` was set first.
|
||||||
|
|
||||||
|
### `@discardableResult func setConstraints(_ anchorType: AnchorType, _ value: CGFloat, _ safeArea: Bool = false) -> UIView`
|
||||||
|
Positional shorthand for the reuse form — the one you chain.
|
||||||
|
|
||||||
|
### `func setConstraints(_ toScrollView: UIScrollView, direction: UICollectionView.ScrollDirection = .vertical)`
|
||||||
|
Pin `self` as the single content view of a scroll view (edges + matching
|
||||||
|
width/height, with a low-priority constraint on the scroll axis). Traps if
|
||||||
|
`self` is itself a `UIScrollView`.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
scrollView.addSubview(content)
|
||||||
|
content.setConstraints(scrollView, direction: .vertical)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Constraint accessors
|
||||||
|
`widthConstraint`, `heightConstraint`, `leadingConstraint`, `trailingConstraint`,
|
||||||
|
`topConstraint`, `bottomConstraint`, `centerXConstraints`, `centerYConstraints` —
|
||||||
|
the first matching `NSLayoutConstraint` found by walking up the hierarchy.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
box.widthConstraint?.constant = 120
|
||||||
|
UIView.animate(withDuration: 0.2) { self.view.layoutIfNeeded() }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func findConstraint(attribute: NSLayoutConstraint.Attribute, for view: UIView) -> NSLayoutConstraint?`
|
||||||
|
Search self and ancestors for a constraint on `attribute` involving `view`.
|
||||||
|
|
||||||
|
### `func constraints(on anchor:) -> [NSLayoutConstraint]`
|
||||||
|
Constraints touching a given `NSLayoutYAxisAnchor` / `XAxisAnchor` / `Dimension` of `self`.
|
||||||
|
|
||||||
|
### `@discardableResult func setHeight(size:) -> Self` / `setHeight(min:)` / `setWidth(size:)` / `setWidth(min:)`
|
||||||
|
Activate a fixed or minimum dimension constraint.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
avatar.setWidth(size: 44).setHeight(size: 44)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func setWidth(_ toView: UIView? = nil, constant: CGFloat, _ multiplier: CGFloat = 0) -> Self` / `setHeight(...)`
|
||||||
|
Dimension relative to another view (`multiplier`) or fixed (`multiplier == 0`).
|
||||||
|
|
||||||
|
### Frame setters
|
||||||
|
`setX(x:)`, `setY(y:)`, `setFrameWidth(width:)`, `setFrameHeight(height:)` — mutate
|
||||||
|
`self.frame` directly (manual layout, not Auto Layout).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UIView</b> — hierarchy, appearance, effects</summary>
|
||||||
|
|
||||||
|
### `static var className: String`
|
||||||
|
`String(describing: self)`.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
UIButton.className // "UIButton"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func addSubview(_ subview:, translatesAutoresizingMaskIntoConstraints: Bool = false)` / `addSubviews(_ subviews: [UIView], …)`
|
||||||
|
Add a view (or many) and set `translatesAutoresizingMaskIntoConstraints` in one call.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
view.addSubviews([header, body, footer]) // all ready for Auto Layout
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func subviews<T>(ofType _: T.Type) -> [T]`
|
||||||
|
All descendants of a type, recursively.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
formView.subviews(ofType: UITextField.self)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func findAView<T>(_ ofType: T.Type) -> T?`
|
||||||
|
First descendant of a type.
|
||||||
|
|
||||||
|
### `var parentViewController: UIViewController?`
|
||||||
|
Nearest owning view controller (walks the responder chain).
|
||||||
|
|
||||||
|
### `var borderColor: UIColor?` / `var borderWidth: CGFloat` / `var cornerRadius: CGFloat`
|
||||||
|
Layer border/corner shortcuts (`cornerRadius` also sets `masksToBounds`).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
card.cornerRadius = 12
|
||||||
|
card.borderWidth = 1
|
||||||
|
card.borderColor = .separator
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func setRadius(top: Bool, bottom: Bool, radius: CGFloat = 8)`
|
||||||
|
Round only the top and/or bottom corners.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
sheet.setRadius(top: true, bottom: false, radius: 16)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func applyShadow(color:offSet:radius:opacity:shouldRasterize: Bool = true, rasterizationScaleTo: = UIScreen.main.scale)` / `func removeShadow()`
|
||||||
|
Layer shadow with rasterisation on by default.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
card.applyShadow(color: .black, offSet: CGSize(width: 0, height: 2),
|
||||||
|
radius: 6, opacity: 0.15)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func insertBlurView(style: UIBlurEffect.Style, color: UIColor = .black, alpha: CGFloat = 0.9)`
|
||||||
|
Insert a full-bleed `UIVisualEffectView` behind the content.
|
||||||
|
|
||||||
|
### `func drawCircle(inCoord x:y:with radius:strokeColor: = .red, fillColor: = .gray, isEmpty: Bool = false) -> [String: Any]`
|
||||||
|
Add a circular `CAShapeLayer`; returns `["path": UIBezierPath, "layer": CAShapeLayer]`.
|
||||||
|
|
||||||
|
### `var isRightToLeft: Bool`
|
||||||
|
Effective RTL layout direction.
|
||||||
|
|
||||||
|
### `var screenshot: UIImage?` / `func asImage() -> UIImage`
|
||||||
|
Render the view to an image (`screenshot` via the legacy context, `asImage` via `UIGraphicsImageRenderer`).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let png = chartView.asImage().pngData()
|
||||||
|
```
|
||||||
|
|
||||||
|
### `var globalPoint: CGPoint?` / `var globalFrame: CGRect?` / `var absolutePosition: CGRect`
|
||||||
|
Origin/frame converted to window coordinates.
|
||||||
|
|
||||||
|
### `func fadeIn(withDuration: TimeInterval = 1, withDelay: TimeInterval = 0, completionHandler: @escaping (Bool) -> Void)` / `func fadeOut(...)`
|
||||||
|
Animate `alpha` to 1 / 0. *(Completion-handler API — pre-dates async/await.)*
|
||||||
|
|
||||||
|
```swift
|
||||||
|
overlay.fadeOut { _ in overlay.removeFromSuperview() }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `var viewReference: UIView?`
|
||||||
|
Scratch reference used internally by the chained `setConstraintsTo` calls.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>GradientOrientation / EnumBorderSide</b></summary>
|
||||||
|
|
||||||
|
### `enum GradientOrientation`
|
||||||
|
`topRightBottomLeft`, `topLeftBottomRight`, `horizontal`, `vertical` — maps to a
|
||||||
|
`CAGradientLayer` start/end point pair. Passed to gradient helpers elsewhere in
|
||||||
|
the package.
|
||||||
|
|
||||||
|
### `enum EnumBorderSide`
|
||||||
|
`top`, `bottom`, `left`, `right` — which edge to draw a single-side border on.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UIStackView</b> — convenience init, bulk arranged-subview ops (iOS 9+)</summary>
|
||||||
|
|
||||||
|
### `convenience init(arrangedSubviews: [UIView]? = nil, axis: = .vertical, spacing: = 0, alignment: = .fill, distribution: = .fill, layoutMargins: = .zero, isMarginsRelative: Bool = true)`
|
||||||
|
One-call configured stack view.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let stack = UIStackView(arrangedSubviews: [title, subtitle],
|
||||||
|
axis: .vertical, spacing: 4)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func addArrangedSubviews(_ views: [UIView], translateAutoresizing: Bool = false)`
|
||||||
|
Add many arranged subviews, setting their autoresizing flag.
|
||||||
|
|
||||||
|
### `func removeAllArrangedSubviews(deactivateConstraints: Bool = true)`
|
||||||
|
Remove and dispose every arranged subview.
|
||||||
|
|
||||||
|
### `func removeSubview(view: UIView, deactivateConstraints: Bool = true)`
|
||||||
|
Remove one arranged subview (optionally deactivating its constraints and removing from the hierarchy).
|
||||||
|
|
||||||
|
### `func addSpace(_ size: CGFloat, backgroundColor: UIColor = .clear)`
|
||||||
|
Insert a fixed-size spacer view sized along the stack's axis.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
stack.addArrangedSubviews([row1, row2])
|
||||||
|
stack.addSpace(24)
|
||||||
|
stack.addArrangedSubview(row3)
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
> `NSLayoutConstraint` helpers (`constraintWithMultiplier`, `matches(view:anchor:)`)
|
||||||
|
> and `[NSLayoutConstraint].filtered(view:anchor:)` are `internal` — used by the
|
||||||
|
> `UIView.constraints(on:)` accessors above, not called directly.
|
||||||
|
|
||||||
|
## Views & Controls
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UIButton</b> — per-state accessors, all-state setters</summary>
|
||||||
|
|
||||||
|
### Per-state properties
|
||||||
|
`imageForNormal` / `imageForHighlighted` / `imageForSelected` / `imageForDisabled`,
|
||||||
|
`titleForNormal` / `…Highlighted` / `…Selected` / `…Disabled`,
|
||||||
|
`titleColorForNormal` / `…Highlighted` / `…Selected` / `…Disabled` — get/set
|
||||||
|
shortcuts for the matching `UIControl.State` (also `@IBInspectable`).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
button.titleForNormal = "Save"
|
||||||
|
button.titleColorForDisabled = .tertiaryLabel
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func setTitleForAllStates(_:)` / `func setTitleColorForAllStates(_:)` / `func setImageForAllStates(_:)`
|
||||||
|
Apply one value to `.normal`, `.selected`, `.highlighted`, `.disabled` at once.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
button.setTitleColorForAllStates(.white)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func centerTextAndImage(spacing: CGFloat)`
|
||||||
|
Balance title/image edge insets so text + icon sit centred with `spacing` between them.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
button.centerTextAndImage(spacing: 8)
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UILabel</b></summary>
|
||||||
|
|
||||||
|
### `func lineNumbers() -> Int`
|
||||||
|
Rendered line count at the current width.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
if bodyLabel.lineNumbers() > 3 { showMoreButton() }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `var getEstimatedHeight: CGFloat`
|
||||||
|
Height the label would need to show its full text/attributed text unclipped.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UITextField</b></summary>
|
||||||
|
|
||||||
|
### `var placeholderColor: UIColor`
|
||||||
|
Get/set the placeholder text colour (rebuilds `attributedPlaceholder`; setter is a
|
||||||
|
no-op if no placeholder text is set).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
field.placeholder = "Email"
|
||||||
|
field.placeholderColor = .secondaryLabel
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func addPaddingLeft(_ padding: CGFloat)`
|
||||||
|
Inset the text from the left with an empty spacer view.
|
||||||
|
|
||||||
|
### `func addPaddingLeftIcon(_ image: UIImage, padding: CGFloat)`
|
||||||
|
Left view = an icon plus trailing padding.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
field.addPaddingLeftIcon(UIImage(systemName: "magnifyingglass")!, padding: 8)
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UIImageView</b> (<code>@MainActor</code>)</summary>
|
||||||
|
|
||||||
|
### `func changeColorOfImage(_ color: UIColor, image: UIImage?) -> UIImageView`
|
||||||
|
Set a template-rendered image tinted to `color`; returns `self`.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
iconView.changeColorOfImage(.systemBlue, image: UIImage(named: "star"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### `var encodeToBase64: String?`
|
||||||
|
JPEG (quality 0.6) of the current image as a Base64 string.
|
||||||
|
|
||||||
|
### `func addAspectRatioConstraint()` / `func removeAspectRatioConstraint()`
|
||||||
|
Add / remove a width-to-height constraint matching the current image's ratio.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
photoView.image = photo
|
||||||
|
photoView.addAspectRatioConstraint()
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UIImage</b> — recolour, resize, thumbnails, masks, init</summary>
|
||||||
|
|
||||||
|
### `func imageWithColor(color: UIColor) -> UIImage` / `func tintImage(color: UIColor) -> UIImage`
|
||||||
|
Return a copy filled / tinted with `color` (keeps the alpha shape).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let redIcon = icon.tintImage(color: .systemRed)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func backgroundColorTransparent(initialColor: UIColor, finalColor: UIColor) -> UIImage?`
|
||||||
|
Make pixels in a colour range transparent.
|
||||||
|
|
||||||
|
### `class func outlinedEllipse(size: CGSize, color: UIColor, lineWidth: CGFloat = 1) -> UIImage?`
|
||||||
|
Generate a stroked-ellipse image.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
UIImage.outlinedEllipse(size: CGSize(width: 24, height: 24), color: .label)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func resizeImage(newWidth: CGFloat) -> UIImage`
|
||||||
|
Scale to `newWidth`, keeping the aspect ratio.
|
||||||
|
|
||||||
|
### `func createThumbnail(_ maxPixelSize: UInt) -> UIImage`
|
||||||
|
Fast down-sampled thumbnail via `CGImageSource`.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let thumb = fullImage.createThumbnail(200)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func maskWithAlphaImage(maskImage: UIImage) -> UIImage`
|
||||||
|
Use another image's alpha as a mask.
|
||||||
|
|
||||||
|
### `func isAnimated() -> Bool`
|
||||||
|
Whether the image has more than one frame.
|
||||||
|
|
||||||
|
### `init?(base64String: String, scale: CGFloat = 1)`
|
||||||
|
Decode a Base64 string to an image.
|
||||||
|
|
||||||
|
### `init(view: UIView)` — *`@MainActor`*
|
||||||
|
Rasterise a view into an image.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let snapshot = UIImage(view: cardView)
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UIColor</b> — hex, components</summary>
|
||||||
|
|
||||||
|
### `convenience init(hex: String)`
|
||||||
|
Parse `#RGB`, `#RGBA`, `#RRGGBB`, or `#RRGGBBAA` (with or without `#`).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
view.backgroundColor = UIColor(hex: "#1E88E5")
|
||||||
|
UIColor(hex: "FF0000CC") // red at 80% alpha
|
||||||
|
```
|
||||||
|
|
||||||
|
### `var hexString: String?`
|
||||||
|
`#RRGGBB` (or `#RRGGBBAA` when alpha < 1).
|
||||||
|
|
||||||
|
### `var redValue` / `var greenValue` / `var blueValue` / `var alphaValue`
|
||||||
|
Individual channel values (`CGFloat`, via `CIColor`).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
UIColor.systemBlue.redValue // 0.0…1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UIScrollView</b> — snapshot, visible rect, paged scrolling</summary>
|
||||||
|
|
||||||
|
### `var snapshot: UIImage?`
|
||||||
|
Image of the **entire** content size (not just the visible part) — works on
|
||||||
|
`UITableView` / `UICollectionView` too.
|
||||||
|
|
||||||
|
### `var visibleRect: CGRect`
|
||||||
|
The currently visible content region.
|
||||||
|
|
||||||
|
### `var offsetInPage: CGFloat`
|
||||||
|
Fractional position within the current page height (`0.0`…`1.0`).
|
||||||
|
|
||||||
|
### `func scrollUp(animated:)` / `scrollDown(animated:)` / `scrollLeft(animated:)` / `scrollRight(animated:)`
|
||||||
|
Move one page (respects `isPagingEnabled`). `animated` defaults to `true`.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
nextButton.onTap = { scrollView.scrollRight() }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `enum orientation`
|
||||||
|
`horizontal` / `vertical` — helper enum used by scroll utilities.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UITapGestureRecognizer</b></summary>
|
||||||
|
|
||||||
|
### `func didTapAttributedTextInLabel(label: UILabel, textToTouch: String) -> Bool`
|
||||||
|
Whether the tap landed on a given substring of a label's attributed text — for
|
||||||
|
making part of a label tappable.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
@objc func handleTap(_ g: UITapGestureRecognizer) {
|
||||||
|
if g.didTapAttributedTextInLabel(label: termsLabel, textToTouch: "Terms of Use") {
|
||||||
|
openTerms()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Navigation & Controllers
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UINavigationController</b> — completion-handler push/pop, transparent bar</summary>
|
||||||
|
|
||||||
|
### `func pushViewController(_:animated: Bool = true, completion: (() -> Void)? = nil)`
|
||||||
|
### `func popViewController(animated: Bool = true, _ completion: (() -> Void)? = nil)`
|
||||||
|
### `func popToViewController(_:animated: Bool = true, _ completion: (() -> Void)? = nil)`
|
||||||
|
The standard transitions with a completion block (wrapped in a `CATransaction`).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
navigationController?.pushViewController(detail) {
|
||||||
|
print("detail is on screen")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func pushViewController(_:hidesBottomBar: Bool = false, animated: Bool = true)` — *not tvOS*
|
||||||
|
Push while setting `hidesBottomBarWhenPushed`.
|
||||||
|
|
||||||
|
### `func makeTransparent(withTint tint: UIColor = .white)`
|
||||||
|
Clear background + shadow, translucent, tinted bar/title.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
navigationController?.makeTransparent(withTint: .label)
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UIViewController</b> — state, instantiation, dismissal, toasts, keyboard, observers</summary>
|
||||||
|
|
||||||
|
### `var isVisible: Bool` / `var isLoaded: Bool`
|
||||||
|
View is loaded **and** in a window. (`isLoaded` is an alias.)
|
||||||
|
|
||||||
|
### `var isModal: Bool`
|
||||||
|
Whether the controller is presented modally (vs. pushed).
|
||||||
|
|
||||||
|
### `static var className: String` / `static var identifier: String` / `static var segueID: String`
|
||||||
|
`"MyVC"`, `"idMyVC"`, `"idSegueMyVC"`.
|
||||||
|
|
||||||
|
### `static func instantiate<T>(storyBoard: String, identifier: String? = nil, bundle: Bundle? = …) -> T`
|
||||||
|
Load a controller from a storyboard by (default) `T.identifier`.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let vc: ProfileVC = ProfileVC.instantiate(storyBoard: "Main")
|
||||||
|
```
|
||||||
|
|
||||||
|
### `static func instatiate<T>(nibName: String, bundle: Bundle? = nil) -> T` *(sic — "instatiate")*
|
||||||
|
Load from a nib and force an initial layout pass.
|
||||||
|
|
||||||
|
### `func present(viewControllerToPresent:completion: @escaping () -> Void)`
|
||||||
|
`present(_:animated:)` with a completion block.
|
||||||
|
|
||||||
|
### `func closeController(jumpToController: UIViewController? = nil, completion: @escaping () -> Void)`
|
||||||
|
Dismiss if modal, else pop (to `jumpToController` if given), then call `completion`.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
closeController { self.refreshList() }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func show(toastWith message:, font: = system 12, toastPosition: ToastPosition, backgroundColor: = .black, textColor: = .white, duration: = 3)`
|
||||||
|
Show a temporary rounded toast label. `ToastPosition` is `.top` / `.down`
|
||||||
|
(notch-aware at the top).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
show(toastWith: "Saved", toastPosition: .down)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func addNotificationObserver(name:selector:)` / `func removeNotificationObserver(name:)` / `func removeNotificationsObserver()`
|
||||||
|
`NotificationCenter` registration shortcuts (last one removes all).
|
||||||
|
|
||||||
|
### `@objc func dismissSystemKeyboard(_ sender: UITapGestureRecognizer)`
|
||||||
|
Ready-made selector for a tap-to-dismiss-keyboard gesture.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
view.addGestureRecognizer(UITapGestureRecognizer(target: self,
|
||||||
|
action: #selector(dismissSystemKeyboard(_:))))
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UITabBarController</b> — badges, animated tab switching</summary>
|
||||||
|
|
||||||
|
### `func setBadges(badgeValues: [Int], font: UIFont = Helvetica-Light 11)`
|
||||||
|
Set numeric badges for every tab at once (`0` = no badge). Custom-drawn
|
||||||
|
(`CustomTabBadge` label), so they position above each tab item.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
tabBarController?.setBadges(badgeValues: [0, 3, 0, 12])
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func addBadge(index:value:color:font:)`
|
||||||
|
Add a single custom badge.
|
||||||
|
|
||||||
|
### `func setSelectedView(atIndex:withAnimation: Bool = false, completion:)`
|
||||||
|
Select a tab (pops that tab's nav stack to root first).
|
||||||
|
|
||||||
|
### `func setSelectedView(withNoPop atIndex:withAnimation: Bool = false, completion:)`
|
||||||
|
Same, without popping to root.
|
||||||
|
|
||||||
|
### `func animateToTab(toIndex: Int)`
|
||||||
|
Slide-transition between tabs.
|
||||||
|
|
||||||
|
### `func changeViewControllerToItem(withViewController:Item:)`
|
||||||
|
Replace a tab's root controller then switch to that tab.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
tabBarController?.changeViewControllerToItem(withViewController: NewHome(), Item: 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `class CustomTabBadge: UILabel`
|
||||||
|
The badge label type used above (`init(font:)`).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UIApplication</b> — environment, app info, open URL</summary>
|
||||||
|
|
||||||
|
### `enum Environment` + `static var inferredEnvironment: Environment`
|
||||||
|
`.debug` / `.testFlight` / `.appStore`, inferred from build config, simulator, and
|
||||||
|
the provisioning/receipt files.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
if UIApplication.inferredEnvironment == .appStore { enableAnalytics() }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `static var displayName: String?` / `static var buildNumber: String?` / `static var version: String?`
|
||||||
|
Bundle info values. (`LCEssentials.appVersion` etc. forward to these.)
|
||||||
|
|
||||||
|
### `static func openURL(urlStr: String)`
|
||||||
|
Open a URL string if it can be opened.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
UIApplication.openURL(urlStr: "https://loverde.com.br")
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UIResponder</b></summary>
|
||||||
|
|
||||||
|
### `var getParentViewController: UIViewController?`
|
||||||
|
Walk the responder chain to the owning view controller.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
someView.getParentViewController?.present(alert, animated: true)
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UIDevice</b> — notch metrics, model name</summary>
|
||||||
|
|
||||||
|
### `static var topNotch: CGFloat` / `static var bottomNotch: CGFloat` / `static var hasNotch: Bool`
|
||||||
|
Safe-area top/bottom insets and whether the device has a notch / Dynamic Island.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let headerY: CGFloat = UIDevice.hasNotch ? 44 : 20
|
||||||
|
```
|
||||||
|
|
||||||
|
### `var modelName: String`
|
||||||
|
Marketing name from the hardware identifier (`"iPhone 15 Pro"`, `"iPad Air (5th generation)"`, …); falls back to the raw identifier for unknown devices. Reads `SIMULATOR_MODEL_IDENTIFIER` on the simulator.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
UIDevice.current.modelName // "iPhone 16 Pro"
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Collections & Tables
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UITableView</b> — typed dequeue, safe indexing, cell animations</summary>
|
||||||
|
|
||||||
|
### `func dequeueReusableCell<T: UITableViewCell>(withClass name: T.Type) -> T` / `…(withClass:for indexPath:) -> T`
|
||||||
|
Dequeue a cell by its class name as the identifier. **Traps** if the cell isn't registered.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let cell = tableView.dequeueReusableCell(withClass: OrderCell.self, for: indexPath)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func dequeueReusableHeaderFooterView<T: UITableViewHeaderFooterView>(withClass name: T.Type) -> T`
|
||||||
|
Same, for section header/footer views.
|
||||||
|
|
||||||
|
### `func dequeueCell<T: UITableViewCell>(indexPath: IndexPath) -> T`
|
||||||
|
Dequeue using `T.identifier` (`"id" + class name`).
|
||||||
|
|
||||||
|
### `func reloadData(_ completion: @escaping () -> Void)`
|
||||||
|
`reloadData()` with a callback for when layout settles.
|
||||||
|
|
||||||
|
### `func isValidIndexPath(_:) -> Bool`
|
||||||
|
Bounds check against the current section/row counts.
|
||||||
|
|
||||||
|
### `func safeScrollToRow(at:at scrollPosition:animated:)`
|
||||||
|
`scrollToRow` that silently no-ops for an out-of-range index path.
|
||||||
|
|
||||||
|
### `func makeMoveUpWithFadeAnimation(rowHeight:duration:delayFactor:) -> UITableViewCellAnimation`
|
||||||
|
Build a staggered slide-up + fade-in cell animation closure.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let animator = UITableViewAnimator(
|
||||||
|
animation: tableView.makeMoveUpWithFadeAnimation(rowHeight: 64, duration: 0.35, delayFactor: 0.03)
|
||||||
|
)
|
||||||
|
|
||||||
|
func tableView(_ t: UITableView, willDisplay cell: UITableViewCell, forRowAt ip: IndexPath) {
|
||||||
|
animator.animate(cell: cell, at: ip, in: t)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `typealias UITableViewCellAnimation = (UITableViewCell, IndexPath, UITableView) -> Void`
|
||||||
|
### `class UITableViewAnimator`
|
||||||
|
`init(animation:)` + `animate(cell:at:in:)` — runs a cell animation closure.
|
||||||
|
|
||||||
|
### `UITableViewCell.identifier` / `UITableViewCell.prepareDisclosureIndicator()`
|
||||||
|
`"id" + class name`; and re-tint the disclosure chevron to a template image so it
|
||||||
|
picks up `tintColor`.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>UICollectionView</b> — setup, counts, safe indexing, carousel layout</summary>
|
||||||
|
|
||||||
|
### `static var identifier: String`
|
||||||
|
`"id" + class name`.
|
||||||
|
|
||||||
|
### `func setupCollectionView(flowLayout: = UICollectionViewFlowLayout(), spacings: = 0, direction: = .horizontal, edgesInset: = .zero, allowMulpleSelection: Bool = false, automaticSize: CGSize? = nil)`
|
||||||
|
Configure layout spacing/direction/insets, multi-selection, and self-sizing in one call.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
collectionView.setupCollectionView(spacings: 8, direction: .vertical,
|
||||||
|
edgesInset: .init(top: 12, left: 16, bottom: 12, right: 16))
|
||||||
|
```
|
||||||
|
|
||||||
|
### `func reloadData(_ completion: @escaping () -> Void)`
|
||||||
|
Reload with a completion callback.
|
||||||
|
|
||||||
|
### `func numberOfItems() -> Int`
|
||||||
|
Total items across all sections.
|
||||||
|
|
||||||
|
### `var lastSection: Int` / `var indexPathForLastItem: IndexPath?` / `func indexPathForLastItem(inSection:) -> IndexPath?`
|
||||||
|
Last section index; index path of the last item overall or in a section.
|
||||||
|
|
||||||
|
### `func isValidIndexPath(_:) -> Bool` / `func safeScrollToItem(at:at scrollPosition:animated:)`
|
||||||
|
Bounds check; scroll that no-ops on an invalid index path.
|
||||||
|
|
||||||
|
### `enum CollectionViewFlowLayoutSpacingMode`
|
||||||
|
`.fixed(spacing:)` / `.overlap(visibleOffset:)` — spacing strategy for the carousel layout below.
|
||||||
|
|
||||||
|
### `open class CollectionViewFlowLayout: UICollectionViewFlowLayout`
|
||||||
|
A centred, paginated "cover-flow" style layout: the centred item is full size,
|
||||||
|
side items scale and fade. Tunables: `sideItemScale` (0.6), `sideItemAlpha`
|
||||||
|
(0.6), `sideItemShift` (0), `spacingMode` (`.fixed(40)`).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let layout = CollectionViewFlowLayout()
|
||||||
|
layout.itemSize = CGSize(width: 240, height: 320)
|
||||||
|
layout.sideItemScale = 0.7
|
||||||
|
layout.spacingMode = .overlap(visibleOffset: 30)
|
||||||
|
collectionView.collectionViewLayout = layout
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Media & Components
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>LCSnackBarView</b> — in-app notification banner</summary>
|
||||||
|
|
||||||
|
A chainable banner shown from the top or bottom of a controller. Configure with
|
||||||
|
`configure(...)` calls, then `present()`.
|
||||||
|
|
||||||
|
### `init(style: LCSnackBarViewType = .default, orientation: LCSnackBarOrientation = .top, delegate: LCSnackBarViewDelegate? = nil)`
|
||||||
|
|
||||||
|
### Enums
|
||||||
|
- `LCSnackBarViewType` — `.default` (rectangular) / `.rounded`
|
||||||
|
- `LCSnackBarOrientation` — `.top` / `.bottom`
|
||||||
|
- `LCSnackBarTimer: CGFloat` — `.infinity` (0, manual dismiss), `.minimum` (2s), `.medium` (5s), `.maximum` (10s)
|
||||||
|
|
||||||
|
### Configuration (each returns `Self`)
|
||||||
|
| Method | Sets |
|
||||||
|
|---|---|
|
||||||
|
| `configure(text: String)` | the message |
|
||||||
|
| `configure(textColor: UIColor)` | text colour |
|
||||||
|
| `configure(textFont: UIFont, alignment: NSTextAlignment = .center)` | font + alignment |
|
||||||
|
| `configure(backgroundColor: UIColor)` | banner background |
|
||||||
|
| `configure(exibition timer: LCSnackBarTimer)` | how long it stays |
|
||||||
|
| `configure(imageIconBefore icon: UIImageView, withTintColor: UIColor? = nil)` | leading icon |
|
||||||
|
|
||||||
|
### `func present(completion: (() -> Void)? = nil)`
|
||||||
|
Show on the top-most view controller.
|
||||||
|
|
||||||
|
### `weak var delegate: LCSnackBarViewDelegate?`
|
||||||
|
`snackbar(didStartExibition:)`, `snackbar(didTouchOn:)`, `snackbar(didEndExibition:)` — all optional.
|
||||||
|
|
||||||
|
### Full example
|
||||||
|
|
||||||
|
```swift
|
||||||
|
LCSnackBarView(style: .rounded, orientation: .bottom)
|
||||||
|
.configure(text: "Profile saved")
|
||||||
|
.configure(backgroundColor: .systemGreen)
|
||||||
|
.configure(exibition: .minimum)
|
||||||
|
.present()
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>ImagePickerController</b> — camera / photo-library picker with permissions</summary>
|
||||||
|
|
||||||
|
Wraps `UIImagePickerController`, handling camera & photo-library authorization
|
||||||
|
(including the "go to Settings" path) and presenting a source-choice alert.
|
||||||
|
|
||||||
|
### `init()`
|
||||||
|
### `weak var delegate: ImagePickerControllerDelegate?`
|
||||||
|
### `var isEditable: Bool` — allow in-picker cropping (default `false`)
|
||||||
|
### `func openImagePicker()`
|
||||||
|
Check permissions, then present the camera/library choice.
|
||||||
|
|
||||||
|
### `protocol ImagePickerControllerDelegate: AnyObject`
|
||||||
|
`imagePicker(didSelect image: UIImage?)` — the picked (or edited) image, or `nil` on cancel/failure.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let picker = ImagePickerController()
|
||||||
|
picker.delegate = self
|
||||||
|
picker.isEditable = true
|
||||||
|
present(picker, animated: false) { picker.openImagePicker() }
|
||||||
|
|
||||||
|
// ImagePickerControllerDelegate
|
||||||
|
func imagePicker(didSelect image: UIImage?) {
|
||||||
|
avatarView.image = image
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>ImageZoomController</b> — full-screen pinch-zoom / pan viewer</summary>
|
||||||
|
|
||||||
|
### `init(_ withImage: UIImage)`
|
||||||
|
### `var minimumZoomScale: CGFloat` (default `1.0`) / `var maximumZoomScale: CGFloat` (default `6.0`)
|
||||||
|
### `var addGestureToDismiss: Bool` (default `true`) — drag down to close
|
||||||
|
### `weak var delegate: ImageZoomControllerDelegate?`
|
||||||
|
### `func present(completion: (() -> Void)? = nil)` / `func dismiss(completion: (() -> Void)? = nil)`
|
||||||
|
Present from / dismiss to the top-most controller.
|
||||||
|
|
||||||
|
### `@objc protocol ImageZoomControllerDelegate`
|
||||||
|
`imageZoomController(controller:didZoom:)`, `imageZoomController(controller:didClose:)` — both optional.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let viewer = ImageZoomController(photo)
|
||||||
|
viewer.maximumZoomScale = 4
|
||||||
|
viewer.present()
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>GifHelper</b> — animated GIF loading</summary>
|
||||||
|
|
||||||
|
### `UIImageView.loadGif(name: String)` / `UIImageView.loadGif(asset: String)` *(iOS 9+)*
|
||||||
|
Decode a bundled `.gif` (or an asset-catalog data set) off the main thread and
|
||||||
|
assign the resulting animated `UIImage`.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
bannerView.loadGif(name: "loading") // loading.gif in the bundle
|
||||||
|
bannerView.loadGif(asset: "confetti") // NSDataAsset "confetti"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `UIImage.gif(data: Data) -> UIImage?` / `gif(url: String) -> UIImage?` / `gif(name: String) -> UIImage?` / `gif(asset: String) -> UIImage?`
|
||||||
|
Build an animated `UIImage` from GIF bytes / a URL string / a bundled file / an
|
||||||
|
asset-catalog entry. Frame delays are honoured (via the GCD of per-frame delays).
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let spinner = UIImage.gif(name: "spinner")
|
||||||
|
imageView.image = spinner
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
82
README.md
82
README.md
@@ -1,85 +1,43 @@
|
|||||||
|
|
||||||

|

|
||||||
Loverde Co. Essentials Swift Scripts
|
|
||||||
----
|
|
||||||
|
|
||||||
This is a repository of essential scripts written in Swift for Loverde Co. used to save time on re-writing and keeping it on all other projects. So this Cocoapods will evolve with Swift and will improve with every release!
|
# Loverde Co. Essentials
|
||||||
|
|
||||||
|
Essential Swift scripts, extensions, SwiftUI components, and UIKit-era helpers,
|
||||||
|
shared across Loverde Co. projects. Evolves with Swift, improves every release.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
- iOS 15.* or newer, Swift 5.* or newer.
|
|
||||||
|
|
||||||
## Features
|
- iOS 15 or newer · Swift 5 or newer
|
||||||
- [x] Many usefull scripts extensions
|
|
||||||
- [x] `API` — typed async networking + multipart uploads. See **[Documentation/API.md](Documentation/API.md)**
|
|
||||||
|
|
||||||
|
## Installation — Swift Package Manager
|
||||||
|
|
||||||
Installation
|
|
||||||
----
|
|
||||||
#### Swift Package Manager (SPM)
|
|
||||||
```swift
|
```swift
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials", .upToNextMajor(from: "1.0.0"))
|
.package(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials", .upToNextMajor(from: "2.0.0"))
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also add it via XCode SPM editor with URL:
|
Or add it in Xcode via **File ▸ Add Package Dependencies…** with the URL:
|
||||||
|
|
||||||
``` swift
|
```
|
||||||
https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials
|
https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage example
|
|
||||||
|
|
||||||
* Background Trhead
|
|
||||||
|
|
||||||
```swift
|
|
||||||
LCEssentials.backgroundThread(delay: 0.6, background: {
|
|
||||||
//Do something im background
|
|
||||||
}) {
|
|
||||||
//When finish, update UI
|
|
||||||
}
|
|
||||||
```
|
|
||||||
* NavigationController with Completion Handler
|
|
||||||
|
|
||||||
```swift
|
|
||||||
self.navigationController?.popViewControllerWithHandler(completion: {
|
|
||||||
//Do some stuff after pop
|
|
||||||
})
|
|
||||||
|
|
||||||
//or more simple
|
|
||||||
self.navigationController?.popViewControllerWithHandler {
|
|
||||||
//Do some stuff after pop
|
|
||||||
}
|
|
||||||
```
|
|
||||||
* Networking with `API`
|
|
||||||
|
|
||||||
```swift
|
|
||||||
struct User: Decodable, Sendable { let id: Int; let name: String }
|
|
||||||
|
|
||||||
let user: User = try await API.shared.request(
|
|
||||||
url: "https://api.example.com/users/{id}",
|
|
||||||
method: .get,
|
|
||||||
pathParams: ["id": "42"]
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Full guide — requests, uploads, client certificates, error handling, and why it
|
|
||||||
beats a hand-rolled `URLSession`: **[Documentation/API.md](Documentation/API.md)**
|
|
||||||
|
|
||||||
## Another components
|
|
||||||
> LCESnackBarView - **great way to send feedback to user**
|
|
||||||
|
|
||||||
And then import `LCEssentials ` wherever you import UIKit or SwiftUI
|
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
import LCEssentials
|
import LCEssentials
|
||||||
```
|
```
|
||||||
|
|
||||||
Any question or doubts, please send thru email
|
## Documentation
|
||||||
|
|
||||||
Daniel Arantes Loverde - <daniel@loverde.com.br>
|
| Guide | Covers |
|
||||||
|
| --- | --- |
|
||||||
|
| **[API.md](Documentation/API.md)** | `API` networking — typed requests, multipart uploads, client certificates, error handling, and why it beats a hand-rolled `URLSession` |
|
||||||
|
| **[Extensions.md](Documentation/Extensions.md)** | Foundation / value-type / string / collection / numeric / date / crypto extensions and the `LCEssentials` namespace |
|
||||||
|
| **[SwiftUI.md](Documentation/SwiftUI.md)** | SwiftUI components (`LCENavigationView`) and `View` helpers |
|
||||||
|
| **[UIKit.md](Documentation/UIKit.md)** | Programmatic layout & constraints, view/control extensions, navigation, tables, and drop-in components (`LCSnackBarView`, image picker/zoom, GIF loading) |
|
||||||
|
|
||||||
[](https://github.com/loverde-co/resume/)
|
---
|
||||||
[](https://github.com/loverde-co)
|
|
||||||
|
|
||||||
Autor: Daniel Arantes Loverde
|
Daniel Arantes Loverde — <daniel@loverde.com.br>
|
||||||
|
|
||||||
|
[<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" width="28" alt="GitHub">](https://github.com/loverde-co/resume/)
|
||||||
|
|||||||
@@ -49,6 +49,39 @@ class LCENavigationState: ObservableObject {
|
|||||||
@Published var subTitle: (any View) = Text("")
|
@Published var subTitle: (any View) = Text("")
|
||||||
/// The background color of the navigation bar.
|
/// The background color of the navigation bar.
|
||||||
@Published var navigationBarBackgroundColor: Color = .clear
|
@Published var navigationBarBackgroundColor: Color = .clear
|
||||||
|
|
||||||
|
/// Whether a left button was actually configured via `setLeftButton`.
|
||||||
|
@Published var hasLeftButton: Bool = false
|
||||||
|
/// Whether a right button was actually configured via `setRightButton`.
|
||||||
|
@Published var hasRightButton: Bool = false
|
||||||
|
/// Whether buttons should opt into the system Liquid Glass button style (iOS 26+). Off by default.
|
||||||
|
@Published var useGlassButtons: Bool = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `PreferenceKey` used to measure the widest side button so the title can be padded symmetrically and stay centered without overlapping either button.
|
||||||
|
@available(iOS 15, *)
|
||||||
|
private struct LCENavButtonWidthPreferenceKey: PreferenceKey {
|
||||||
|
static var defaultValue: CGFloat { 0 }
|
||||||
|
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||||
|
value = max(value, nextValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 15, *)
|
||||||
|
private extension View {
|
||||||
|
/// Applies `.glass` button style on iOS 26+ when enabled, otherwise falls back to `.plain` — the navigation bar is not designed around glass, but stays opt-in ready.
|
||||||
|
@ViewBuilder
|
||||||
|
func lce_applyGlassIfEnabled(_ enabled: Bool) -> some View {
|
||||||
|
if enabled {
|
||||||
|
if #available(iOS 26.0, *) {
|
||||||
|
self.buttonStyle(.glass)
|
||||||
|
} else {
|
||||||
|
self.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `LCENavigationView` is a SwiftUI `View` that provides a customizable navigation bar.
|
/// `LCENavigationView` is a SwiftUI `View` that provides a customizable navigation bar.
|
||||||
@@ -61,6 +94,9 @@ public struct LCENavigationView<Content: View>: View {
|
|||||||
/// The content view displayed below the navigation bar.
|
/// The content view displayed below the navigation bar.
|
||||||
let content: Content
|
let content: Content
|
||||||
|
|
||||||
|
/// The measured width of the widest side button, used to pad the title so it never overlaps either button.
|
||||||
|
@State private var maxButtonWidth: CGFloat = 0
|
||||||
|
|
||||||
/// Initializes a new `LCENavigationView` instance.
|
/// Initializes a new `LCENavigationView` instance.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - title: The title view for the navigation bar. Defaults to an empty `Text`.
|
/// - title: The title view for the navigation bar. Defaults to an empty `Text`.
|
||||||
@@ -92,18 +128,29 @@ public struct LCENavigationView<Content: View>: View {
|
|||||||
|
|
||||||
/// The private `NavigationBarView` that lays out the navigation bar components.
|
/// The private `NavigationBarView` that lays out the navigation bar components.
|
||||||
private var NavigationBarView: some View {
|
private var NavigationBarView: some View {
|
||||||
HStack {
|
ZStack {
|
||||||
NavLeftButton
|
|
||||||
Spacer()
|
|
||||||
TitleView
|
TitleView
|
||||||
|
.padding(.horizontal, maxButtonWidth)
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.7)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
if state.hasLeftButton {
|
||||||
|
NavLeftButton
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
|
if state.hasRightButton {
|
||||||
NavRightButton
|
NavRightButton
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.padding()
|
.padding()
|
||||||
.background {
|
.background {
|
||||||
state.navigationBarBackgroundColor.ignoresSafeArea(edges: .top)
|
state.navigationBarBackgroundColor.ignoresSafeArea(edges: .top)
|
||||||
}
|
}
|
||||||
|
.onPreferenceChange(LCENavButtonWidthPreferenceKey.self) { maxButtonWidth = $0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The private `TitleView` that displays the title and subtitle.
|
/// The private `TitleView` that displays the title and subtitle.
|
||||||
@@ -116,7 +163,7 @@ public struct LCENavigationView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The private `NavLeftButton` view.
|
/// The private `NavLeftButton` view. Only rendered when `setLeftButton` was actually called — never a hidden placeholder.
|
||||||
private var NavLeftButton: some View {
|
private var NavLeftButton: some View {
|
||||||
Button(action: state.leftButtonAction) {
|
Button(action: state.leftButtonAction) {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -126,9 +173,15 @@ public struct LCENavigationView<Content: View>: View {
|
|||||||
state.leftButtonText
|
state.leftButtonText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.lce_applyGlassIfEnabled(state.useGlassButtons)
|
||||||
|
.background(
|
||||||
|
GeometryReader { proxy in
|
||||||
|
Color.clear.preference(key: LCENavButtonWidthPreferenceKey.self, value: proxy.size.width)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The private `NavRightButton` view.
|
/// The private `NavRightButton` view. Only rendered when `setRightButton` was actually called — never a hidden placeholder.
|
||||||
private var NavRightButton: some View {
|
private var NavRightButton: some View {
|
||||||
Button(action: state.rightButtonAction) {
|
Button(action: state.rightButtonAction) {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -138,6 +191,12 @@ public struct LCENavigationView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.lce_applyGlassIfEnabled(state.useGlassButtons)
|
||||||
|
.background(
|
||||||
|
GeometryReader { proxy in
|
||||||
|
Color.clear.preference(key: LCENavButtonWidthPreferenceKey.self, value: proxy.size.width)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the configuration for the right button of the navigation bar.
|
/// Sets the configuration for the right button of the navigation bar.
|
||||||
@@ -158,13 +217,7 @@ public struct LCENavigationView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
state.rightButtonText = text
|
state.rightButtonText = text
|
||||||
state.rightButtonAction = action
|
state.rightButtonAction = action
|
||||||
|
state.hasRightButton = true
|
||||||
if let string = state.leftButtonText.string, string.isEmpty {
|
|
||||||
state.leftButtonText = text.foregroundColor(.clear)
|
|
||||||
}
|
|
||||||
if state.leftButtonImage == nil {
|
|
||||||
state.leftButtonImage = image?.foregroundColor(.clear) as? AnyView
|
|
||||||
}
|
|
||||||
|
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
@@ -187,14 +240,17 @@ public struct LCENavigationView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
state.leftButtonText = text
|
state.leftButtonText = text
|
||||||
state.leftButtonAction = action
|
state.leftButtonAction = action
|
||||||
|
state.hasLeftButton = true
|
||||||
|
|
||||||
if let string = state.rightButtonText.string, string.isEmpty {
|
return self
|
||||||
state.rightButtonText = text.foregroundColor(.clear)
|
|
||||||
}
|
|
||||||
if state.rightButtonImage == nil {
|
|
||||||
state.rightButtonImage = image?.foregroundColor(.clear) as? AnyView
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Opts the navigation bar's buttons into the system Liquid Glass `.glass` button style on iOS 26+.
|
||||||
|
/// The navigation bar is designed as a plain, non-glass component by default; call this to enable glass explicitly.
|
||||||
|
/// - Parameter enabled: Whether buttons should use the glass style.
|
||||||
|
/// - Returns: The `LCENavigationView` instance for chaining.
|
||||||
|
public func setGlassButtonsEnabled(_ enabled: Bool) -> LCENavigationView {
|
||||||
|
state.useGlassButtons = enabled
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,101 +285,6 @@ public struct LCENavigationView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extension to `FormatStyle` to format any value as a string.
|
|
||||||
@available(iOS 15.0, *)
|
|
||||||
extension FormatStyle {
|
|
||||||
/// Formats an input value if it matches the `FormatInput` type.
|
|
||||||
/// - Parameter value: The value to format as `Any`.
|
|
||||||
/// - Returns: The formatted output, or `nil` if the value type does not match.
|
|
||||||
func format(any value: Any) -> FormatOutput? {
|
|
||||||
if let v = value as? FormatInput {
|
|
||||||
return format(v)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extension to `LocalizedStringKey` to resolve localized strings.
|
|
||||||
@available(iOS 15.0, *)
|
|
||||||
extension LocalizedStringKey {
|
|
||||||
/// Resolves the localized string key into a `String`.
|
|
||||||
/// - Returns: The resolved string, or `nil` if resolution fails.
|
|
||||||
var resolved: String? {
|
|
||||||
let mirror = Mirror(reflecting: self)
|
|
||||||
guard let key = mirror.descendant("key") as? String else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let args = mirror.descendant("arguments") as? [Any] else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let values = args.map { arg -> Any? in
|
|
||||||
let mirror = Mirror(reflecting: arg)
|
|
||||||
if let value = mirror.descendant("storage", "value", ".0") {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let format = mirror.descendant("storage", "formatStyleValue", "format") as? any FormatStyle,
|
|
||||||
let input = mirror.descendant("storage", "formatStyleValue", "input") else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return format.format(any: input)
|
|
||||||
}
|
|
||||||
|
|
||||||
let va = values.compactMap { arg -> CVarArg? in
|
|
||||||
switch arg {
|
|
||||||
case let i as Int: return i
|
|
||||||
case let i as Int64: return i
|
|
||||||
case let i as Int8: return i
|
|
||||||
case let i as Int16: return i
|
|
||||||
case let i as Int32: return i
|
|
||||||
case let u as UInt: return u
|
|
||||||
case let u as UInt64: return u
|
|
||||||
case let u as UInt8: return u
|
|
||||||
case let u as UInt16: return u
|
|
||||||
case let u as UInt32: return u
|
|
||||||
case let f as Float: return f
|
|
||||||
case let f as CGFloat: return f
|
|
||||||
case let d as Double: return d
|
|
||||||
case let o as NSObject: return o
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if va.count != values.count {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return String.localizedStringWithFormat(key, va)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extension to `Text` to retrieve its string content.
|
|
||||||
@available(iOS 15.0, *)
|
|
||||||
extension Text {
|
|
||||||
/// Returns the string representation of the `Text` view.
|
|
||||||
/// - Returns: The string content, or `nil` if it cannot be extracted.
|
|
||||||
var string: String? {
|
|
||||||
let mirror = Mirror(reflecting: self)
|
|
||||||
if let s = mirror.descendant("storage", "verbatim") as? String {
|
|
||||||
return s
|
|
||||||
} else if let attrStr = mirror.descendant("storage", "anyTextStorage", "str") as? AttributedString {
|
|
||||||
return String(attrStr.characters)
|
|
||||||
} else if let key = mirror.descendant("storage", "anyTextStorage", "key") as? LocalizedStringKey {
|
|
||||||
return key.resolved
|
|
||||||
} else if let format = mirror.descendant("storage", "anyTextStorage", "storage", "format") as? any FormatStyle,
|
|
||||||
let input = mirror.descendant("storage", "anyTextStorage", "storage", "input") {
|
|
||||||
return format.format(any: input) as? String
|
|
||||||
} else if let formatter = mirror.descendant("storage", "anyTextStorage", "formatter") as? Formatter,
|
|
||||||
let object = mirror.descendant("storage", "anyTextStorage", "object") {
|
|
||||||
return formatter.string(for: object)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//@available(iOS 15.0, *)
|
//@available(iOS 15.0, *)
|
||||||
//struct LCENavigationView_Previews: PreviewProvider {
|
//struct LCENavigationView_Previews: PreviewProvider {
|
||||||
// static var previews: some View {
|
// static var previews: some View {
|
||||||
|
|||||||
Reference in New Issue
Block a user