Compare commits
5 Commits
docs/refer
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d5067212a | |||
|
|
72a5b59c44 | ||
|
|
d678965154 | ||
|
|
0cceda1ad9 | ||
|
|
2cd52d3d12 |
109
README.md
109
README.md
@@ -1,94 +1,43 @@
|
||||

|
||||
|
||||

|
||||
Loverde Co. Essentials Swift Scripts
|
||||
----
|
||||
# Loverde Co. Essentials
|
||||
|
||||
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!
|
||||
Essential Swift scripts, extensions, SwiftUI components, and UIKit-era helpers,
|
||||
shared across Loverde Co. projects. Evolves with Swift, improves every release.
|
||||
|
||||
## Requirements
|
||||
- iOS 15.* or newer, Swift 5.* or newer.
|
||||
|
||||
## Features
|
||||
- [x] Many usefull scripts extensions
|
||||
- [x] `API` — typed async networking + multipart uploads
|
||||
- [x] SwiftUI components (`LCENavigationView`) and UIKit-era helpers
|
||||
- iOS 15 or newer · Swift 5 or newer
|
||||
|
||||
## Installation — Swift Package Manager
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials", .upToNextMajor(from: "2.0.0"))
|
||||
]
|
||||
```
|
||||
|
||||
Or add it in Xcode via **File ▸ Add Package Dependencies…** with the URL:
|
||||
|
||||
```
|
||||
https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials
|
||||
```
|
||||
|
||||
```swift
|
||||
import LCEssentials
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
| Guide | Covers |
|
||||
| --- | --- |
|
||||
| **[API.md](Documentation/API.md)** | `API` networking — requests, multipart uploads, client certificates, error handling, and why it beats a hand-rolled `URLSession` |
|
||||
| **[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 and `View` helpers |
|
||||
| **[UIKit.md](Documentation/UIKit.md)** | Programmatic layout & constraints, view/control extensions, navigation, tables, and drop-in components |
|
||||
| **[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) |
|
||||
|
||||
---
|
||||
|
||||
Installation
|
||||
----
|
||||
#### Swift Package Manager (SPM)
|
||||
``` swift
|
||||
dependencies: [
|
||||
.package(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials", .upToNextMajor(from: "1.0.0"))
|
||||
]
|
||||
```
|
||||
Daniel Arantes Loverde — <daniel@loverde.com.br>
|
||||
|
||||
You can also add it via XCode SPM editor with URL:
|
||||
|
||||
``` swift
|
||||
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
|
||||
import LCEssentials
|
||||
```
|
||||
|
||||
Any question or doubts, please send thru email
|
||||
|
||||
Daniel Arantes Loverde - <daniel@loverde.com.br>
|
||||
|
||||
[](https://github.com/loverde-co/resume/)
|
||||
|
||||
Autor: Daniel Arantes Loverde
|
||||
[<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" width="28" alt="GitHub">](https://github.com/loverde-co/resume/)
|
||||
|
||||
@@ -42,13 +42,46 @@ class LCENavigationState: ObservableObject {
|
||||
|
||||
/// A boolean value that controls the visibility of the navigation bar.
|
||||
@Published var hideNavigationBar: Bool = false
|
||||
|
||||
|
||||
/// The title view of the navigation bar.
|
||||
@Published var title: (any View) = Text("")
|
||||
/// The subtitle view of the navigation bar.
|
||||
@Published var subTitle: (any View) = Text("")
|
||||
/// The background color of the navigation bar.
|
||||
@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.
|
||||
@@ -60,7 +93,10 @@ public struct LCENavigationView<Content: View>: View {
|
||||
|
||||
/// The content view displayed below the navigation bar.
|
||||
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.
|
||||
/// - Parameters:
|
||||
/// - title: The title view for the navigation bar. Defaults to an empty `Text`.
|
||||
@@ -92,20 +128,31 @@ public struct LCENavigationView<Content: View>: View {
|
||||
|
||||
/// The private `NavigationBarView` that lays out the navigation bar components.
|
||||
private var NavigationBarView: some View {
|
||||
HStack {
|
||||
NavLeftButton
|
||||
Spacer()
|
||||
ZStack {
|
||||
TitleView
|
||||
Spacer()
|
||||
NavRightButton
|
||||
.padding(.horizontal, maxButtonWidth)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.7)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
HStack {
|
||||
if state.hasLeftButton {
|
||||
NavLeftButton
|
||||
}
|
||||
Spacer()
|
||||
if state.hasRightButton {
|
||||
NavRightButton
|
||||
}
|
||||
}
|
||||
}
|
||||
.font(.headline)
|
||||
.padding()
|
||||
.background {
|
||||
state.navigationBarBackgroundColor.ignoresSafeArea(edges: .top)
|
||||
}
|
||||
.onPreferenceChange(LCENavButtonWidthPreferenceKey.self) { maxButtonWidth = $0 }
|
||||
}
|
||||
|
||||
|
||||
/// The private `TitleView` that displays the title and subtitle.
|
||||
private var TitleView: some View {
|
||||
VStack {
|
||||
@@ -115,8 +162,8 @@ 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 {
|
||||
Button(action: state.leftButtonAction) {
|
||||
HStack {
|
||||
@@ -126,9 +173,15 @@ public struct LCENavigationView<Content: View>: View {
|
||||
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 {
|
||||
Button(action: state.rightButtonAction) {
|
||||
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.
|
||||
@@ -158,14 +217,8 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
state.rightButtonText = text
|
||||
state.rightButtonAction = action
|
||||
|
||||
if let string = state.leftButtonText.string, string.isEmpty {
|
||||
state.leftButtonText = text.foregroundColor(.clear)
|
||||
}
|
||||
if state.leftButtonImage == nil {
|
||||
state.leftButtonImage = image?.foregroundColor(.clear) as? AnyView
|
||||
}
|
||||
|
||||
state.hasRightButton = true
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
@@ -187,14 +240,17 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
state.leftButtonText = text
|
||||
state.leftButtonAction = action
|
||||
|
||||
if let string = state.rightButtonText.string, string.isEmpty {
|
||||
state.rightButtonText = text.foregroundColor(.clear)
|
||||
}
|
||||
if state.rightButtonImage == nil {
|
||||
state.rightButtonImage = image?.foregroundColor(.clear) as? AnyView
|
||||
}
|
||||
|
||||
state.hasLeftButton = true
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
@@ -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, *)
|
||||
//struct LCENavigationView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
|
||||
Reference in New Issue
Block a user