Compare commits
5 Commits
docs/refer
...
2.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d5067212a | |||
|
|
72a5b59c44 | ||
|
|
d678965154 | ||
|
|
0cceda1ad9 | ||
|
|
2cd52d3d12 |
91
README.md
91
README.md
@@ -1,94 +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
|
|
||||||
- [x] SwiftUI components (`LCENavigationView`) and UIKit-era helpers
|
|
||||||
|
|
||||||
## Documentation
|
## Installation — Swift Package Manager
|
||||||
|
|
||||||
| Guide | Covers |
|
|
||||||
| --- | --- |
|
|
||||||
| **[API.md](Documentation/API.md)** | `API` networking — 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 and `View` helpers |
|
|
||||||
| **[UIKit.md](Documentation/UIKit.md)** | Programmatic layout & constraints, view/control extensions, navigation, tables, and drop-in components |
|
|
||||||
|
|
||||||
|
|
||||||
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/)
|
---
|
||||||
|
|
||||||
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