Compare commits
15 Commits
8f8cffc8ba
...
main
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4f84dfb108 | ||
08519d1aca | |||
![]() |
7ac2ccb21f | ||
![]() |
d2ca6e54d2 | ||
![]() |
0eb4f355df | ||
![]() |
3e3e181b36 | ||
![]() |
175816dff8 | ||
![]() |
daae48817a | ||
![]() |
241d69ecc1 | ||
![]() |
f4fade0442 | ||
![]() |
0910973e9a | ||
![]() |
189efd7154 | ||
![]() |
578c69e0f8 | ||
![]() |
f0f9f1bace | ||
![]() |
6040caa2de |
15
Package.resolved
Normal file
15
Package.resolved
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"originHash" : "c64eb123858a637db7dce26475a35db7adb60ead911617c384beba36afa95bc5",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "lcecryptokitbinary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://60c260c85d3a2fe840411b0ff98f521b5eca3c56@git.loverde.com.br/Loverde-Company-LTDA/LCECryptoKitBinary.git",
|
||||
"state" : {
|
||||
"revision" : "2e7850fdb14cacf6bf2eb160f64c3df84cf5b1c4",
|
||||
"version" : "1.0.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
@@ -1,16 +1,33 @@
|
||||
// swift-tools-version: 6.0
|
||||
import PackageDescription
|
||||
import Foundation
|
||||
|
||||
let isLocalDevelopment = FileManager.default.fileExists(atPath: "../LCECryptoKit/PrivateLib/LCECryptoKitBinary")
|
||||
|
||||
let package = Package(
|
||||
name: "LCEssentials",
|
||||
platforms: [
|
||||
.iOS(.v13),
|
||||
.macOS(.v10_15),
|
||||
.tvOS(.v13),
|
||||
.watchOS(.v6)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "LCEssentials",
|
||||
targets: ["LCEssentials"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(
|
||||
url: isLocalDevelopment ?
|
||||
"../LCECryptoKit/PrivateLib/LCECryptoKitBinary" :
|
||||
"https://60c260c85d3a2fe840411b0ff98f521b5eca3c56@git.loverde.com.br/Loverde-Company-LTDA/LCECryptoKitBinary.git",
|
||||
exact: "1.0.0"
|
||||
)
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "LCEssentials"),
|
||||
|
||||
name: "LCEssentials",
|
||||
dependencies: [.product(name: "LCECryptoKit", package: "lcecryptokitbinary")]),
|
||||
]
|
||||
)
|
||||
|
@@ -17,14 +17,14 @@ Installation
|
||||
#### Swift Package Manager (SPM)
|
||||
``` swift
|
||||
dependencies: [
|
||||
.package(url: "http://git.loverde.com.br:3000/git/LCEssentials.git", .upToNextMajor(from: "1.0.0"))
|
||||
.package(url: "https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials", .upToNextMajor(from: "1.0.0"))
|
||||
]
|
||||
```
|
||||
|
||||
You can also add it via XCode SPM editor with URL:
|
||||
|
||||
``` swift
|
||||
http://git.loverde.com.br:3000/git/LCEssentials.git
|
||||
https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials
|
||||
```
|
||||
|
||||
## Usage example
|
||||
|
41
Sources/LCEssentials/Classes/LCECrypto.swift
Normal file
41
Sources/LCEssentials/Classes/LCECrypto.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// Copyright (c) 2025 Loverde Co.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
|
||||
import Foundation
|
||||
import LCECryptoKit
|
||||
|
||||
public final class LCECrypto {
|
||||
|
||||
private let hashKey: String
|
||||
|
||||
init(privateKey: String){
|
||||
self.hashKey = privateKey
|
||||
}
|
||||
|
||||
func encodeOTP(email: String, password: String) -> String? {
|
||||
return LCECryptoKit.encodeSeed(email: email, password: password, hashKey: self.hashKey)
|
||||
}
|
||||
|
||||
func decodeOTP(_ otpHash: String) -> Bool {
|
||||
LCECryptoKit.decodeSeed(otpKey: otpHash, hashKey: self.hashKey)
|
||||
}
|
||||
}
|
@@ -29,18 +29,30 @@ import Security
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
/// A generic `Result` enumeration to represent either a success `Value` or a failure `Error`.
|
||||
public enum Result<Value, Error: Swift.Error> {
|
||||
/// Indicates a successful operation with an associated `Value`.
|
||||
case success(Value)
|
||||
/// Indicates a failed operation with an associated `Error`.
|
||||
case failure(Error)
|
||||
}
|
||||
|
||||
/// Enumeration defining common HTTP methods.
|
||||
public enum httpMethod: String {
|
||||
/// The POST method.
|
||||
case post = "POST"
|
||||
/// The GET method.
|
||||
case get = "GET"
|
||||
/// The PUT method.
|
||||
case put = "PUT"
|
||||
/// The DELETE method.
|
||||
case delete = "DELETE"
|
||||
}
|
||||
/// Loverde Co.: API generic struct for simple requests
|
||||
|
||||
/// Loverde Co.: API generic struct for simple requests.
|
||||
///
|
||||
/// This struct provides a convenient way to perform network requests with various configurations,
|
||||
/// including handling different HTTP methods, parameter encoding, and certificate-based authentication.
|
||||
@available(iOS 13.0.0, *)
|
||||
@MainActor
|
||||
public struct API {
|
||||
@@ -48,20 +60,43 @@ public struct API {
|
||||
private static var certData: Data?
|
||||
private static var certPassword: String?
|
||||
|
||||
/// The default error used when an unexpected issue occurs during a request.
|
||||
static let defaultError = NSError.createErrorWith(code: LCEssentials.DEFAULT_ERROR_CODE,
|
||||
description: LCEssentials.DEFAULT_ERROR_MSG,
|
||||
reasonForError: LCEssentials.DEFAULT_ERROR_MSG)
|
||||
|
||||
/// The delay in seconds before retrying a persistent connection request.
|
||||
public static var persistConnectionDelay: Double = 3
|
||||
|
||||
/// Default parameters that will be included in all requests unless explicitly overridden.
|
||||
public static var defaultParams: [String:Any] = [String: Any]()
|
||||
|
||||
/// Default HTTP headers for requests.
|
||||
///
|
||||
/// By default, it includes "Accept", "Content-Type", and "Accept-Encoding" headers.
|
||||
var defaultHeaders: [String: String] = ["Accept": "application/json",
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
"Accept-Encoding": "gzip"]
|
||||
|
||||
/// The shared singleton instance of the `API` struct.
|
||||
public static let shared = API()
|
||||
|
||||
private init(){}
|
||||
|
||||
/// Performs an asynchronous network request and decodes the response into a `Codable` type.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The URL string for the request.
|
||||
/// - params: Optional parameters for the request. Can be `[String: Any]` for JSON/form-data, or `Data` for raw body.
|
||||
/// - method: The HTTP method to use for the request (`.get`, `.post`, `.put`, `.delete`).
|
||||
/// - headers: Optional custom HTTP headers to be added to the request. These override default headers if there are conflicts.
|
||||
/// - jsonEncoding: A boolean indicating whether parameters should be JSON encoded. Defaults to `true`.
|
||||
/// - debug: A boolean indicating whether to print debug logs for the request and response. Defaults to `true`.
|
||||
/// - timeoutInterval: The timeout interval in seconds for the request. Defaults to `30`.
|
||||
/// - networkServiceType: The `URLRequest.NetworkServiceType` for the request. Defaults to `.default`.
|
||||
/// - persistConnection: A boolean indicating whether to persist the connection on certain error codes (e.g., 4xx). Defaults to `false`.
|
||||
/// - Returns: An instance of the `T` type, decoded from the response data.
|
||||
/// - Throws: An `Error` if the request fails, including `URLError` for network issues or `DecodingError` for JSON decoding failures.
|
||||
public func request<T: Codable>(url: String,
|
||||
params: Any? = nil,
|
||||
method: httpMethod,
|
||||
@@ -83,7 +118,7 @@ public struct API {
|
||||
|
||||
var body = Data()
|
||||
|
||||
// Adiciona campos adicionais (se houver)
|
||||
// Add additional fields (if any)
|
||||
for (key, value) in params where key != "file" {
|
||||
let stringValue = "\(value)"
|
||||
body.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||||
@@ -91,25 +126,36 @@ public struct API {
|
||||
body.append("\(stringValue)\r\n".data(using: .utf8)!)
|
||||
}
|
||||
|
||||
// Adiciona o arquivo
|
||||
// Add the file
|
||||
let fileName = fileURL.lastPathComponent
|
||||
let mimeType = mimeTypeForPath(path: fileName)
|
||||
printInfo(title: "Body size before", msg: "\(body.count) bytes")
|
||||
|
||||
let fileData: Data
|
||||
do {
|
||||
fileData = try Data(contentsOf: fileURL)
|
||||
printInfo(title: "Body size after", msg: "\(body.count) bytes")
|
||||
} catch {
|
||||
printError(title: "Upload File", msg: error.localizedDescription)
|
||||
throw error
|
||||
}
|
||||
|
||||
do {
|
||||
let fileData = try Data(contentsOf: fileURL)
|
||||
body.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||||
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
|
||||
body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
|
||||
body.append(fileData)
|
||||
body.append("\r\n".data(using: .utf8)!)
|
||||
} catch {
|
||||
printError(title: "Upload File", msg: error.localizedDescription)
|
||||
let fileDataCopy = Data(fileData)
|
||||
body.append(fileDataCopy)
|
||||
let dataUTF8 = "\r\n".data(using: .utf8)!
|
||||
body.append(dataUTF8)
|
||||
printInfo(title: "Body size after", msg: "\(body.count) bytes")
|
||||
}
|
||||
|
||||
// Finaliza o corpo da requisição
|
||||
// Finalize the request body
|
||||
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
|
||||
request.httpBody = body
|
||||
request.setValue("\(body.count)", forHTTPHeaderField: "Content-Length")
|
||||
// Logs de depuração
|
||||
// Debug logs
|
||||
printLog(title: "Boundary", msg: boundary)
|
||||
if let bodyString = String(data: body, encoding: .utf8) {
|
||||
printLog(title: "Body Content", msg: bodyString)
|
||||
@@ -131,7 +177,7 @@ public struct API {
|
||||
request.timeoutInterval = timeoutInterval
|
||||
request.networkServiceType = networkServiceType
|
||||
|
||||
// - Put Default Headers togheter with user defined params
|
||||
// - Put Default Headers together with user defined params
|
||||
if !headers.isEmpty {
|
||||
// - Add it to request
|
||||
headers.forEach { (key, value) in
|
||||
@@ -187,6 +233,7 @@ public struct API {
|
||||
}
|
||||
if persistConnection {
|
||||
printError(title: "INTERNET CONNECTION ERROR", msg: "WILL PERSIST")
|
||||
// Recursive call for persistence
|
||||
let persist: T = try await self.request(
|
||||
url: url,
|
||||
params: params,
|
||||
@@ -221,6 +268,11 @@ public struct API {
|
||||
throw API.defaultError
|
||||
}
|
||||
|
||||
/// Sets up client certificate data and an optional password for authentication.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - certData: The `Data` representation of the client certificate (e.g., a .p12 file).
|
||||
/// - password: The password for the certificate, if required. Defaults to an empty string.
|
||||
public func setupCertificationRequest(certData: Data, password: String = "") {
|
||||
API.certData = certData
|
||||
API.certPassword = password
|
||||
@@ -228,6 +280,8 @@ public struct API {
|
||||
}
|
||||
|
||||
#if canImport(Security)
|
||||
/// A custom `URLSessionDelegate` handler for managing URL session challenges,
|
||||
/// particularly for client and server trust authentication.
|
||||
@available(iOS 13.0.0, *)
|
||||
@MainActor
|
||||
private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
|
||||
@@ -235,24 +289,38 @@ private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
|
||||
private var certData: Data?
|
||||
private var certPass: String?
|
||||
|
||||
/// Initializes a new `URLSessionDelegateHandler` instance.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - certData: Optional `Data` for the client certificate.
|
||||
/// - password: Optional password for the client certificate.
|
||||
init(certData: Data? = nil, password: String? = nil) {
|
||||
super.init()
|
||||
self.certData = certData
|
||||
self.certPass = password
|
||||
}
|
||||
|
||||
/// Handles URL session authentication challenges.
|
||||
///
|
||||
/// This method is responsible for providing client certificates for client certificate
|
||||
/// authentication and validating server trust for server authentication.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - session: The URL session that issued the challenge.
|
||||
/// - challenge: The authentication challenge.
|
||||
/// - Returns: A tuple containing the disposition for the challenge and the credential to use.
|
||||
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
|
||||
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
|
||||
// Carregar o certificado do cliente
|
||||
// Load the client certificate
|
||||
guard let identity = getIdentity() else {
|
||||
return (URLSession.AuthChallengeDisposition.performDefaultHandling, nil)
|
||||
}
|
||||
|
||||
// Criar o URLCredential com a identidade
|
||||
// Create the URLCredential with the identity
|
||||
let credential = URLCredential(identity: identity, certificates: nil, persistence: .forSession)
|
||||
return (URLSession.AuthChallengeDisposition.useCredential, credential)
|
||||
} else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
|
||||
// Validar o certificado do servidor
|
||||
// Validate the server certificate
|
||||
if let serverTrust = challenge.protectionSpace.serverTrust {
|
||||
let serverCredential = URLCredential(trust: serverTrust)
|
||||
return (URLSession.AuthChallengeDisposition.useCredential, serverCredential)
|
||||
@@ -264,13 +332,16 @@ private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the `SecIdentity` from the provided certificate data and password.
|
||||
///
|
||||
/// - Returns: A `SecIdentity` object if the certificate is successfully imported, otherwise `nil`.
|
||||
private func getIdentity() -> SecIdentity? {
|
||||
guard let certData = self.certData else { return nil }
|
||||
// Especifique a senha usada ao exportar o .p12
|
||||
// Specify the password used when exporting the .p12
|
||||
let options: [String: Any] = [kSecImportExportPassphrase as String: self.certPass ?? ""]
|
||||
var items: CFArray?
|
||||
|
||||
// Importar o certificado .p12 para obter a identidade
|
||||
// Import the .p12 certificate to get the identity
|
||||
let status = SecPKCS12Import(certData as CFData, options as CFDictionary, &items)
|
||||
|
||||
if status == errSecSuccess,
|
||||
@@ -285,6 +356,12 @@ private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
|
||||
|
||||
}
|
||||
|
||||
/// Checks if the provided data is a valid PKCS#12 (P12) certificate with the given password.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - data: The `Data` representation of the certificate.
|
||||
/// - password: The password for the certificate.
|
||||
/// - Returns: `true` if the data is a valid P12 certificate with the given password, otherwise `false`.
|
||||
private func isPKCS12(data: Data, password: String) -> Bool {
|
||||
let options: [String: Any] = [kSecImportExportPassphrase as String: password]
|
||||
|
||||
@@ -297,6 +374,7 @@ private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
|
||||
#endif
|
||||
|
||||
extension Error {
|
||||
/// Returns the status code of the error, if available.
|
||||
public var statusCode: Int {
|
||||
get{
|
||||
return self._code
|
||||
@@ -307,6 +385,11 @@ extension Error {
|
||||
@available(iOS 13.0.0, *)
|
||||
extension API {
|
||||
|
||||
/// Logs details of an outgoing network request for debugging purposes.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - method: The HTTP method of the request.
|
||||
/// - request: The `URLRequest` object.
|
||||
fileprivate static func requestLOG(method: httpMethod, request: URLRequest) {
|
||||
|
||||
print("\n<========================= 🟠 INTERNET CONNECTION - REQUEST =========================>")
|
||||
@@ -325,6 +408,14 @@ extension API {
|
||||
print("<======================================================================================>")
|
||||
}
|
||||
|
||||
/// Logs details of an incoming network response for debugging purposes.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - method: The HTTP method of the original request.
|
||||
/// - request: The `URLRequest` object that generated this response.
|
||||
/// - data: The data received in the response.
|
||||
/// - statusCode: The HTTP status code of the response.
|
||||
/// - error: An optional `Error` object if the request failed.
|
||||
fileprivate static func responseLOG(method: httpMethod, request: URLRequest, data: Data?, statusCode: Int, error: Error?) {
|
||||
///
|
||||
let icon = error != nil ? "🔴" : "🟢"
|
||||
@@ -384,11 +475,15 @@ extension API {
|
||||
print("<======================================================================================>")
|
||||
}
|
||||
|
||||
/// Determines the MIME type for a given file path based on its extension.
|
||||
///
|
||||
/// - Parameter path: The file path string.
|
||||
/// - Returns: A string representing the MIME type. Defaults to "application/octet-stream" if the type is unknown.
|
||||
func mimeTypeForPath(path: String) -> String {
|
||||
let url = URL(fileURLWithPath: path)
|
||||
let pathExtension = url.pathExtension.lowercased()
|
||||
|
||||
// Dicionário de extensões e MIME types comuns
|
||||
// Dictionary of common extensions and MIME types
|
||||
let mimeTypes: [String: String] = [
|
||||
"jpg": "image/jpeg",
|
||||
"jpeg": "image/jpeg",
|
||||
@@ -412,7 +507,7 @@ extension API {
|
||||
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
]
|
||||
|
||||
// Retorna o MIME type correspondente à extensão, ou "application/octet-stream" como padrão
|
||||
// Returns the corresponding MIME type for the extension, or "application/octet-stream" as default
|
||||
return mimeTypes[pathExtension] ?? "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
@@ -21,9 +21,11 @@
|
||||
|
||||
|
||||
import Foundation
|
||||
#if canImport(CoreGraphics)
|
||||
import CoreGraphics
|
||||
|
||||
#if canImport(SwiftUI)
|
||||
extension CGPoint: Hashable {
|
||||
extension CGPoint: @retroactive Hashable {
|
||||
// Implementação manual de Hashable
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(x)
|
||||
@@ -35,3 +37,4 @@ extension CGPoint: Hashable {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -30,7 +30,7 @@ public extension CGRect {
|
||||
var center: CGPoint { CGPoint(x: midX, y: midY) }
|
||||
}
|
||||
#if canImport(SwiftUI)
|
||||
extension CGRect: Hashable {
|
||||
extension CGRect: @retroactive Hashable {
|
||||
// Implementação manual de Hashable
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(origin)
|
||||
|
@@ -58,7 +58,8 @@ extension JSONDecoder {
|
||||
let msg = "Type mismatch for type '\(type)' \(T.self) Object: \(context.debugDescription)"
|
||||
error = NSError.createErrorWith(code: 0, description: msg, reasonForError: msg)
|
||||
} catch let DecodingError.valueNotFound(value, context) {
|
||||
let msg = "Missing value '\(value)' in \(T.self) Object in JSON: \(context.debugDescription)"
|
||||
let key = context.codingPath.last?.stringValue ?? "unknown_key"
|
||||
let msg = "Missing value '\(value)' for key '\(key)' in \(T.self) Object in JSON: \(context.debugDescription)"
|
||||
error = NSError.createErrorWith(code: 0, description: msg, reasonForError: msg)
|
||||
} catch {
|
||||
throw error
|
||||
|
@@ -1,271 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2020 Loverde Co.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
@objc public protocol HUDAlertViewControllerDelegate {
|
||||
@objc optional func didOpen(alert: HUDAlertViewController)
|
||||
@objc optional func didClose(alert: HUDAlertViewController)
|
||||
}
|
||||
|
||||
public enum HUDAlertActionType: Int {
|
||||
case cancel = 0
|
||||
case normal = 1
|
||||
case destructive = 2
|
||||
case discrete = 3
|
||||
case green = 4
|
||||
}
|
||||
|
||||
public class HUDAlertAction {
|
||||
|
||||
public typealias CompletionBlock = () -> Void
|
||||
|
||||
var title: String = ""
|
||||
var tag: Int = 0
|
||||
var type: HUDAlertActionType = .cancel
|
||||
var completionBlock: CompletionBlock? = nil
|
||||
|
||||
public convenience init(title: String, type: HUDAlertActionType, _ completion: (() -> Void)? = nil) {
|
||||
self.init()
|
||||
//
|
||||
self.title = title
|
||||
self.type = type
|
||||
self.completionBlock = completion
|
||||
}
|
||||
}
|
||||
|
||||
public class HUDAlertViewController: UIViewController {
|
||||
|
||||
fileprivate var viewController: UIViewController
|
||||
|
||||
lazy var actions: [HUDAlertAction] = []
|
||||
|
||||
let greenColor: UIColor = UIColor(hex: "609c70")
|
||||
let redColor: UIColor = UIColor(hex: "a32a2e")
|
||||
let blueColor: UIColor = UIColor(hex: "2a50a8")
|
||||
let greyColor: UIColor = UIColor(hex: "a6a6a6")
|
||||
|
||||
public weak var delegate: HUDAlertViewControllerDelegate?
|
||||
public var isLoadingAlert: Bool = true
|
||||
|
||||
public var customView: HUDAlertView {
|
||||
return (view as? HUDAlertView) ?? HUDAlertView()
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.viewController = UIViewController()
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required public init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func loadView() {
|
||||
view = HUDAlertView()
|
||||
}
|
||||
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
if isLoaded {
|
||||
printLog(title: "VIEW DIDLOAD", msg: "IS LOADED", prettyPrint: true)
|
||||
}
|
||||
}
|
||||
|
||||
public func setTitle(_ title: String = "", font: UIFont = UIFont.systemFont(ofSize: 16.0), color: UIColor = .black) {
|
||||
self.customView.titleLabel.text = title
|
||||
self.customView.titleLabel.font = font
|
||||
self.customView.titleLabel.textColor = color
|
||||
}
|
||||
|
||||
public func setDescription(_ desc: String = "", font: UIFont = .systemFont(ofSize: 14.0), color: UIColor = .black) {
|
||||
self.customView.descLabel.text = desc
|
||||
self.customView.descLabel.font = font
|
||||
self.customView.descLabel.textColor = color
|
||||
}
|
||||
|
||||
public func configureAlertWith(title: String,
|
||||
description: String? = nil,
|
||||
viewController: UIViewController? = nil,
|
||||
actionButtons: [HUDAlertAction] = []) {
|
||||
|
||||
if let viewController {
|
||||
self.viewController = viewController
|
||||
} else if let topViewController = LCEssentials.getTopViewController(aboveBars: true) {
|
||||
self.viewController = topViewController
|
||||
} else {
|
||||
fatalError("Ops! No UIViewController was found.")
|
||||
}
|
||||
|
||||
if self.isLoadingAlert {
|
||||
self.customView.rotatinProgress.progress = 0.8
|
||||
self.customView.rotatinProgress.heightConstraint?.constant = 40
|
||||
}else{
|
||||
self.customView.rotatinProgress.progress = 0
|
||||
self.customView.rotatinProgress.heightConstraint?.constant = 0
|
||||
}
|
||||
|
||||
self.customView.containerView.cornerRadius = 8
|
||||
self.customView.titleLabel.text = title
|
||||
self.customView.descLabel.text = description
|
||||
self.actions.removeAll()
|
||||
self.customView.stackButtons.removeAllArrangedSubviews()
|
||||
|
||||
if !actionButtons.isEmpty {
|
||||
|
||||
for (index, element) in actionButtons.enumerated() {
|
||||
|
||||
lazy var button: UIButton = {
|
||||
$0.isOpaque = true
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
$0.setTitleForAllStates(element.title)
|
||||
$0.tag = index
|
||||
$0.isExclusiveTouch = true
|
||||
$0.isUserInteractionEnabled = true
|
||||
$0.cornerRadius = 5
|
||||
$0.setHeight(size: 47.0)
|
||||
$0.addTarget(self, action: #selector(self.actionButton(sender:)), for: .touchUpInside)
|
||||
return $0
|
||||
}(UIButton(type: .custom))
|
||||
|
||||
switch element.type {
|
||||
case .cancel:
|
||||
button.borderWidth = 1
|
||||
button.borderColor = self.blueColor
|
||||
button.backgroundColor = .white
|
||||
button.setTitleColorForAllStates(self.blueColor)
|
||||
case .destructive:
|
||||
button.borderWidth = 0
|
||||
button.borderColor = nil
|
||||
button.backgroundColor = self.redColor
|
||||
button.setTitleColorForAllStates(.white)
|
||||
case .normal:
|
||||
button.borderWidth = 0
|
||||
button.borderColor = nil
|
||||
button.backgroundColor = self.blueColor
|
||||
button.setTitleColorForAllStates(.white)
|
||||
case .discrete:
|
||||
button.borderWidth = 0
|
||||
button.borderColor = nil
|
||||
button.backgroundColor = self.greyColor
|
||||
button.setTitleColorForAllStates(.white)
|
||||
case .green:
|
||||
button.borderWidth = 0
|
||||
button.borderColor = nil
|
||||
button.backgroundColor = self.greenColor
|
||||
button.setTitleColorForAllStates(.white)
|
||||
}
|
||||
|
||||
self.customView.stackButtons.isHidden = false
|
||||
self.customView.stackButtons.spacing = 10
|
||||
self.customView.stackButtons.heightConstraint?.isActive = false
|
||||
self.customView.stackButtons.addArrangedSubview(button)
|
||||
self.actions.append(element)
|
||||
}
|
||||
} else {
|
||||
self.actions.removeAll()
|
||||
self.customView.stackButtons.removeAllArrangedSubviews()
|
||||
self.customView.stackButtons.spacing = 0
|
||||
self.customView.stackButtons.heightConstraint?.isActive = true
|
||||
self.customView.stackButtons.heightConstraint?.constant = 0
|
||||
self.customView.stackButtons.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func actionButton(sender: UIButton) {
|
||||
self.actions[exist: sender.tag]?.completionBlock?()
|
||||
self.closeAlert()
|
||||
}
|
||||
|
||||
public func showAlert(){
|
||||
if self.presentingViewController != nil {
|
||||
self.closeAlert()
|
||||
LCEssentials.backgroundThread(delay: 0.2, completion: {
|
||||
self.showAlert()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
self.modalTransitionStyle = .crossDissolve
|
||||
self.modalPresentationStyle = .overFullScreen
|
||||
|
||||
self.viewController.present(self, animated: false, completion: {
|
||||
self.animateIn {
|
||||
self.delegate?.didOpen?(alert: self)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func closeAlert(){
|
||||
animateOut {
|
||||
self.dismiss(animated: false) {
|
||||
self.delegate?.didClose?(alert: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func animateIn(completion: @escaping() -> Void) {
|
||||
|
||||
UIView.animate(withDuration: 0.1, delay: 0.0, options: [.allowUserInteraction, .allowAnimatedContent, .curveEaseOut], animations: {
|
||||
self.view.alpha = 1.0
|
||||
}) { (completed) in
|
||||
self.performSpringAnimationIn(for: self.customView.containerView) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func animateOut(completion: @escaping() -> Void) {
|
||||
|
||||
self.performSpringAnimationOut(for: self.customView.containerView) {
|
||||
UIView.animate(withDuration: 0.1, delay: 0.0, options: [.allowUserInteraction, .allowAnimatedContent, .curveEaseIn], animations: {
|
||||
self.view.alpha = 0.0
|
||||
}) { (completed) in
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func performSpringAnimationIn(for view: UIView, completion: @escaping() -> Void) {
|
||||
view.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
||||
//
|
||||
UIView.animate(withDuration: 0.15, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseIn, animations: {
|
||||
view.transform = CGAffineTransform(scaleX: 1, y: 1)
|
||||
view.alpha = 1.0
|
||||
}) { (completed) in
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func performSpringAnimationOut(for view: UIView, completion: @escaping() -> Void) {
|
||||
UIView.animate(withDuration: 0.15, animations: {
|
||||
view.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
||||
view.alpha = 0.0
|
||||
}) { (completed) in
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@@ -1,158 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2022 Loverde Co.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
|
||||
// MARK: - Framework headers
|
||||
import UIKit
|
||||
|
||||
// MARK: - Protocols
|
||||
|
||||
|
||||
// MARK: - Interface Headers
|
||||
|
||||
|
||||
// MARK: - Local Defines / ENUMS
|
||||
|
||||
|
||||
// MARK: - Class
|
||||
|
||||
public final class HUDAlertView: UIView {
|
||||
|
||||
// MARK: - Private properties
|
||||
|
||||
// MARK: - Internal properties
|
||||
|
||||
lazy var containerView: UIView = {
|
||||
$0.isOpaque = true
|
||||
$0.backgroundColor = .white
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
return $0
|
||||
}(UIView())
|
||||
|
||||
let rotatinProgress: RotatingCircularGradientProgressBar = {
|
||||
$0.layer.masksToBounds = true
|
||||
$0.color = .gray
|
||||
$0.gradientColor = .gray
|
||||
$0.ringWidth = 2
|
||||
$0.progress = 0
|
||||
return $0
|
||||
}(RotatingCircularGradientProgressBar())
|
||||
|
||||
lazy var titleLabel: UILabel = {
|
||||
$0.font = UIFont.systemFont(ofSize: 22.0)
|
||||
$0.textColor = .black
|
||||
$0.backgroundColor = UIColor.clear
|
||||
$0.numberOfLines = 0
|
||||
$0.textAlignment = .center
|
||||
$0.isOpaque = true
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
return $0
|
||||
}(UILabel())
|
||||
|
||||
lazy var descLabel: UILabel = {
|
||||
$0.font = UIFont.systemFont(ofSize: 16.0)
|
||||
$0.textColor = .black
|
||||
$0.backgroundColor = UIColor.clear
|
||||
$0.numberOfLines = 0
|
||||
$0.textAlignment = .center
|
||||
$0.isOpaque = true
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
return $0
|
||||
}(UILabel())
|
||||
|
||||
lazy var stackButtons: UIStackView = {
|
||||
$0.axis = .vertical
|
||||
$0.spacing = 10.0
|
||||
$0.distribution = .fill
|
||||
$0.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
$0.isLayoutMarginsRelativeArrangement = true
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
return $0
|
||||
}(UIStackView())
|
||||
|
||||
// MARK: - Public properties
|
||||
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
public init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
insertBlurView(style: .dark, color: .clear, alpha: 0.8)
|
||||
|
||||
addComponentsAndConstraints()
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Super Class Overrides
|
||||
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
fileprivate func addComponentsAndConstraints() {
|
||||
|
||||
// MARK: - Add Subviews
|
||||
|
||||
containerView.addSubviews([rotatinProgress, titleLabel, descLabel, stackButtons])
|
||||
|
||||
addSubviews([containerView])
|
||||
|
||||
// MARK: - Add Constraints
|
||||
|
||||
rotatinProgress
|
||||
.setConstraintsTo(containerView, .top, 10)
|
||||
.setConstraints(.centerX, 0)
|
||||
rotatinProgress.setWidth(size: 40)
|
||||
rotatinProgress.setHeight(size: 40)
|
||||
|
||||
titleLabel
|
||||
.setConstraintsTo(rotatinProgress, .topToBottom, 8)
|
||||
.setConstraintsTo(containerView, .leading, 10)
|
||||
.setConstraints(.trailing, -10)
|
||||
|
||||
descLabel
|
||||
.setConstraintsTo(titleLabel, .topToBottom, 8)
|
||||
.setConstraintsTo(containerView, .leading, 10)
|
||||
.setConstraints(.trailing, -10)
|
||||
|
||||
stackButtons
|
||||
.setConstraintsTo(descLabel, .topToBottom, 8)
|
||||
.setConstraintsTo(containerView, .leading, 10)
|
||||
.setConstraints(.leading, 10)
|
||||
.setConstraints(.trailing, -10)
|
||||
.setConstraints(.bottom, -8)
|
||||
|
||||
containerView
|
||||
.setConstraintsTo(self, .topToTopGreaterThanOrEqualTo, 20, true)
|
||||
.setConstraints(.leading, 40)
|
||||
.setConstraints(.trailing, -40)
|
||||
.setConstraints(.centerX, 0)
|
||||
.setConstraints(.centerY, 0)
|
||||
}
|
||||
|
||||
// MARK: - Internal methods
|
||||
}
|
||||
|
||||
// MARK: - Extensions
|
Binary file not shown.
Before Width: | Height: | Size: 68 KiB |
Binary file not shown.
Before Width: | Height: | Size: 75 KiB |
@@ -1,4 +1,4 @@
|
||||
//
|
||||
//
|
||||
// Copyright (c) 2023 Loverde Co.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
@@ -25,17 +25,29 @@ import UIKit
|
||||
import AVFoundation
|
||||
import Photos
|
||||
|
||||
/// A protocol that defines the methods an image picker delegate should implement.
|
||||
public protocol ImagePickerControllerDelegate: AnyObject {
|
||||
/// Tells the delegate that an image has been selected by the image picker.
|
||||
/// - Parameter image: The `UIImage` that was selected, or `nil` if the selection was canceled or failed.
|
||||
func imagePicker(didSelect image: UIImage?)
|
||||
}
|
||||
|
||||
/// A custom `UIViewController` that provides functionality for picking images from the camera or photo library.
|
||||
///
|
||||
/// This controller handles permissions for camera and photo library access and presents
|
||||
/// a `UIImagePickerController` to the user.
|
||||
public class ImagePickerController: UIViewController, UINavigationControllerDelegate {
|
||||
|
||||
private var isAlertOpen: Bool = false
|
||||
private var imagePickerController: UIImagePickerController = UIImagePickerController()
|
||||
|
||||
/// The delegate for the `ImagePickerController`, which will receive callbacks when an image is selected.
|
||||
public weak var delegate: ImagePickerControllerDelegate?
|
||||
|
||||
/// A boolean value that determines whether the user can edit the selected image. Defaults to `false`.
|
||||
public var isEditable: Bool = false
|
||||
|
||||
/// Initializes a new `ImagePickerController` instance.
|
||||
public init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
@@ -52,7 +64,10 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
|
||||
self.imagePickerController.mediaTypes = ["public.image", "public.movie"]
|
||||
}
|
||||
|
||||
|
||||
/// Presents the image picker to the user.
|
||||
///
|
||||
/// This method checks for camera and photo library permissions and then presents an alert
|
||||
/// allowing the user to choose between the camera or photo roll, or to grant permissions if needed.
|
||||
public func openImagePicker(){
|
||||
var cameraPerm: Bool = false
|
||||
var albumPerm: Bool = false
|
||||
@@ -85,6 +100,10 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
|
||||
}
|
||||
|
||||
|
||||
/// Presents an `UIAlertController` with options to open the camera, photo roll, or grant permissions.
|
||||
/// - Parameters:
|
||||
/// - forCamera: A boolean indicating whether camera access is granted. Defaults to `true`.
|
||||
/// - forAlbum: A boolean indicating whether photo album access is granted. Defaults to `true`.
|
||||
private func openAlerts(forCamera:Bool = true, forAlbum:Bool = true){
|
||||
let alert = UIAlertController(title: "Choose an option", message: nil, preferredStyle: .actionSheet)
|
||||
if forCamera {
|
||||
@@ -117,6 +136,9 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the camera device using `UIImagePickerController`.
|
||||
///
|
||||
/// If the camera is not available, an alert message is presented to the user.
|
||||
private func openCameraDevice(){
|
||||
if(UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera)){
|
||||
self.imagePickerController.sourceType = UIImagePickerController.SourceType.camera
|
||||
@@ -130,12 +152,14 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the photo library using `UIImagePickerController`.
|
||||
private func openAlbumDevice(){
|
||||
self.imagePickerController.sourceType = UIImagePickerController.SourceType.photoLibrary
|
||||
self.modalPresentationStyle = .fullScreen
|
||||
self.present(self.imagePickerController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
/// Requests photo library access and if denied, guides the user to app settings.
|
||||
private func openAppSettingsPhoto(){
|
||||
PHPhotoLibrary.requestAuthorization({status in
|
||||
if status == .authorized{
|
||||
@@ -160,6 +184,7 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
|
||||
})
|
||||
}
|
||||
|
||||
/// Requests camera access and if denied, guides the user to app settings.
|
||||
private func openAppSettingsCamera(){
|
||||
AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (granted: Bool) -> Void in
|
||||
if granted == true {
|
||||
@@ -189,6 +214,8 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
|
||||
|
||||
extension ImagePickerController: UIImagePickerControllerDelegate {
|
||||
|
||||
/// Tells the delegate that the user canceled the pick operation.
|
||||
/// - Parameter picker: The image picker controller.
|
||||
@objc public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
self.delegate?.imagePicker(didSelect: nil)
|
||||
picker.dismiss(animated: true, completion: {
|
||||
@@ -196,6 +223,10 @@ extension ImagePickerController: UIImagePickerControllerDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
/// Tells the delegate that the user picked an image or movie.
|
||||
/// - Parameters:
|
||||
/// - picker: The image picker controller.
|
||||
/// - info: A dictionary containing the original image, and possibly an edited image or a movie URL.
|
||||
@objc public func imagePickerController(_ picker: UIImagePickerController,
|
||||
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||
if let image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
//
|
||||
//
|
||||
// Copyright (c) 2023 Loverde Co.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
@@ -22,13 +22,25 @@
|
||||
import UIKit
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
/// A protocol for delegates of `ImageZoomController` to provide callbacks for zoom and close events.
|
||||
@objc public protocol ImageZoomControllerDelegate {
|
||||
/// Called when the image in the controller is zoomed.
|
||||
/// - Parameters:
|
||||
/// - controller: The `ImageZoomController` instance.
|
||||
/// - image: The `UIImage` currently displayed and being zoomed.
|
||||
@objc optional func imageZoomController(controller: ImageZoomController, didZoom image: UIImage?)
|
||||
|
||||
/// Called when the image zoom controller is closed.
|
||||
/// - Parameters:
|
||||
/// - controller: The `ImageZoomController` instance.
|
||||
/// - image: The `UIImage` that was displayed when the controller was closed.
|
||||
@objc optional func imageZoomController(controller: ImageZoomController, didClose image: UIImage?)
|
||||
}
|
||||
|
||||
/// A `UIViewController` that allows users to zoom and pan an image, with an option to dismiss by dragging.
|
||||
public class ImageZoomController: UIViewController {
|
||||
|
||||
/// A private lazy `UIView` that serves as a black background with partial opacity.
|
||||
fileprivate lazy var blackView: UIView = {
|
||||
$0.isOpaque = true
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -37,6 +49,7 @@ public class ImageZoomController: UIViewController {
|
||||
return $0
|
||||
}(UIView())
|
||||
|
||||
/// A private lazy `UIScrollView` to enable zooming and panning of the image.
|
||||
fileprivate lazy var scrollView: UIScrollView = {
|
||||
$0.isOpaque = true
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -44,6 +57,7 @@ public class ImageZoomController: UIViewController {
|
||||
return $0
|
||||
}(UIScrollView())
|
||||
|
||||
/// A private lazy `UIButton` to close the image zoom controller.
|
||||
fileprivate lazy var closeButton: UIButton = {
|
||||
$0.isOpaque = true
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -55,6 +69,7 @@ public class ImageZoomController: UIViewController {
|
||||
return $0
|
||||
}(UIButton(type: .custom))
|
||||
|
||||
/// The `UIImageView` that displays the image.
|
||||
lazy var imageView: UIImageView = {
|
||||
$0.isOpaque = true
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -62,16 +77,24 @@ public class ImageZoomController: UIViewController {
|
||||
return $0
|
||||
}(UIImageView())
|
||||
|
||||
/// The minimum zoom scale allowed for the image. Defaults to `1.0`.
|
||||
public var minimumZoomScale: CGFloat = 1.0
|
||||
|
||||
/// The maximum zoom scale allowed for the image. Defaults to `6.0`.
|
||||
public var maximumZoomScale: CGFloat = 6.0
|
||||
|
||||
/// A boolean value indicating whether a pan gesture can be used to dismiss the controller. Defaults to `true`.
|
||||
public var addGestureToDismiss: Bool = true
|
||||
|
||||
/// The delegate for the `ImageZoomController`.
|
||||
public weak var delegate: ImageZoomControllerDelegate?
|
||||
|
||||
private var minimumVelocityToHide: CGFloat = 1500
|
||||
private var minimumScreenRatioToHide: CGFloat = 0.5
|
||||
private var animationDuration: TimeInterval = 0.2
|
||||
|
||||
|
||||
/// Initializes a new `ImageZoomController` with a specified image.
|
||||
/// - Parameter withImage: The `UIImage` to be displayed and zoomed.
|
||||
public init(_ withImage: UIImage) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
@@ -101,6 +124,7 @@ public class ImageZoomController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the subviews and sets up their constraints.
|
||||
fileprivate func addComponentsAndConstraints() {
|
||||
|
||||
scrollView.addSubview(imageView, translatesAutoresizingMaskIntoConstraints: false)
|
||||
@@ -124,6 +148,8 @@ public class ImageZoomController: UIViewController {
|
||||
.setHeight(size: 50.0)
|
||||
}
|
||||
|
||||
/// Presents the `ImageZoomController` from the top-most view controller.
|
||||
/// - Parameter completion: An optional closure to be executed after the presentation is complete.
|
||||
public func present(completion: (()->())? = nil) {
|
||||
guard let viewController = LCEssentials.getTopViewController(aboveBars: true) else {
|
||||
fatalError("Ops! Look like it doesnt have a ViewController")
|
||||
@@ -135,21 +161,29 @@ public class ImageZoomController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismisses the `ImageZoomController`.
|
||||
/// - Parameter completion: An optional closure to be executed after the dismissal is complete.
|
||||
public func dismiss(completion: (()->())? = nil) {
|
||||
self.dismiss(animated: true) {
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
/// Action method for the close button.
|
||||
/// Notifies the delegate that the controller is closing and then dismisses itself.
|
||||
@objc private func close(){
|
||||
delegate?.imageZoomController?(controller: self, didClose: self.imageView.image)
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
/// Slides the view vertically to a specified Y-coordinate.
|
||||
/// - Parameter y: The Y-coordinate to slide the view to.
|
||||
private func slideViewVerticallyTo(_ y: CGFloat) {
|
||||
self.view.frame.origin = CGPoint(x: 0, y: y)
|
||||
}
|
||||
|
||||
/// Handles pan gestures for dismissing the view.
|
||||
/// - Parameter panGesture: The `UIPanGestureRecognizer` instance.
|
||||
@objc private func onPan(_ panGesture: UIPanGestureRecognizer) {
|
||||
|
||||
switch panGesture.state {
|
||||
@@ -194,10 +228,15 @@ public class ImageZoomController: UIViewController {
|
||||
|
||||
extension ImageZoomController: UIScrollViewDelegate {
|
||||
|
||||
/// Tells the delegate that the scroll view has zoomed.
|
||||
/// - Parameter scrollView: The scroll view that zoomed.
|
||||
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||
delegate?.imageZoomController?(controller: self, didZoom: self.imageView.image)
|
||||
}
|
||||
|
||||
/// Returns the view that will be zoomed when the scroll view is zoomed.
|
||||
/// - Parameter scrollView: The scroll view that is zooming.
|
||||
/// - Returns: The view to be zoomed, which is the `imageView` in this case.
|
||||
public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
return self.imageView
|
||||
}
|
||||
|
@@ -24,10 +24,19 @@ import UIKit
|
||||
|
||||
// MARK: - Protocols
|
||||
|
||||
/// A protocol that defines optional methods for `LCSnackBarView` delegate.
|
||||
@objc
|
||||
public protocol LCSnackBarViewDelegate {
|
||||
/// Called when the snackbar starts its exhibition.
|
||||
/// - Parameter didStartExibition: The `LCSnackBarView` instance that started exhibition.
|
||||
@objc optional func snackbar(didStartExibition: LCSnackBarView)
|
||||
|
||||
/// Called when the snackbar is touched.
|
||||
/// - Parameter snackbar: The `LCSnackBarView` instance that was touched.
|
||||
@objc optional func snackbar(didTouchOn snackbar: LCSnackBarView)
|
||||
|
||||
/// Called when the snackbar ends its exhibition.
|
||||
/// - Parameter didEndExibition: The `LCSnackBarView` instance that ended exhibition.
|
||||
@objc optional func snackbar(didEndExibition: LCSnackBarView)
|
||||
}
|
||||
|
||||
@@ -36,23 +45,36 @@ public protocol LCSnackBarViewDelegate {
|
||||
|
||||
// MARK: - Local Defines / ENUMS
|
||||
|
||||
/// Enumeration defining the visual style of the `LCSnackBarView`.
|
||||
public enum LCSnackBarViewType {
|
||||
case `default`, rounded
|
||||
/// The default style, typically rectangular.
|
||||
case `default`
|
||||
/// A rounded style for the snackbar.
|
||||
case rounded
|
||||
}
|
||||
|
||||
/// Enumeration defining the orientation of the `LCSnackBarView`.
|
||||
public enum LCSnackBarOrientation {
|
||||
case top, bottom
|
||||
/// The snackbar appears at the top of the screen.
|
||||
case top
|
||||
/// The snackbar appears at the bottom of the screen.
|
||||
case bottom
|
||||
}
|
||||
|
||||
/// Enumeration defining the display duration for the `LCSnackBarView`.
|
||||
public enum LCSnackBarTimer: CGFloat {
|
||||
/// The snackbar remains visible indefinitely until dismissed manually.
|
||||
case infinity = 0
|
||||
/// Minimum display duration (2 seconds).
|
||||
case minimum = 2
|
||||
/// Medium display duration (5 seconds).
|
||||
case medium = 5
|
||||
/// Maximum display duration (10 seconds).
|
||||
case maximum = 10
|
||||
}
|
||||
|
||||
// MARK: - Class
|
||||
/// LCSnackBarView is a simple SnackBar that you can display notifications in app to improve your app comunication
|
||||
/// `LCSnackBarView` is a simple SnackBar that you can display notifications in-app to improve your app communication.
|
||||
///
|
||||
/// Usage example:
|
||||
///
|
||||
@@ -62,7 +84,7 @@ public enum LCSnackBarTimer: CGFloat {
|
||||
/// .configure(text: "Hello World!")
|
||||
/// .present()
|
||||
///```
|
||||
///You can set delegate to interact with it
|
||||
///You can set a delegate to interact with it:
|
||||
///
|
||||
///```swift
|
||||
///let notification = LCSnackBarView(delegate: self)
|
||||
@@ -78,6 +100,7 @@ public final class LCSnackBarView: UIView {
|
||||
|
||||
// MARK: - Private properties
|
||||
|
||||
/// The content view that holds the snackbar's elements.
|
||||
private lazy var contentView: UIView = {
|
||||
$0.backgroundColor = .white
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -85,6 +108,7 @@ public final class LCSnackBarView: UIView {
|
||||
return $0
|
||||
}(UIView())
|
||||
|
||||
/// The label that displays the main text of the snackbar.
|
||||
private lazy var descriptionLabel: UILabel = {
|
||||
$0.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
$0.text = nil
|
||||
@@ -117,10 +141,16 @@ public final class LCSnackBarView: UIView {
|
||||
|
||||
// MARK: - Public properties
|
||||
|
||||
/// The delegate for the snackbar view.
|
||||
public weak var delegate: LCSnackBarViewDelegate?
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
/// Initializes a new `LCSnackBarView` instance.
|
||||
/// - Parameters:
|
||||
/// - style: The visual style of the snackbar. Defaults to `.default`.
|
||||
/// - orientation: The orientation (top or bottom) of the snackbar. Defaults to `.top`.
|
||||
/// - delegate: The delegate to receive snackbar events. Defaults to `nil`.
|
||||
public init(
|
||||
style: LCSnackBarViewType = .default,
|
||||
orientation: LCSnackBarOrientation = .top,
|
||||
@@ -161,12 +191,14 @@ public extension LCSnackBarView {
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
/// Sets up the default layout properties for the snackbar.
|
||||
private func setupDefaultLayout() {
|
||||
backgroundColor = .white
|
||||
contentView.backgroundColor = .white
|
||||
clipsToBounds = true
|
||||
}
|
||||
|
||||
/// Sets up a tap gesture recognizer for the snackbar.
|
||||
private func setupGestureRecognizer() {
|
||||
let gesture = UITapGestureRecognizer(target: self, action: #selector(onTapGestureAction))
|
||||
gesture.numberOfTapsRequired = 1
|
||||
@@ -174,6 +206,7 @@ public extension LCSnackBarView {
|
||||
addGestureRecognizer(gesture)
|
||||
}
|
||||
|
||||
/// Configures observers for keyboard appearance and disappearance notifications.
|
||||
private func setKeyboardObserver() {
|
||||
// Show
|
||||
NotificationCenter
|
||||
@@ -196,6 +229,8 @@ public extension LCSnackBarView {
|
||||
)
|
||||
}
|
||||
|
||||
/// Handles the `keyboardWillShowNotification` to adjust the snackbar's position.
|
||||
/// - Parameter notification: The `Notification` object containing keyboard information.
|
||||
@objc private func keyboardWillShow(_ notification: Notification?) -> Void {
|
||||
|
||||
if let info = notification?.userInfo {
|
||||
@@ -236,6 +271,8 @@ public extension LCSnackBarView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the `keyboardWillHideNotification`.
|
||||
/// - Parameter notification: The `Notification` object.
|
||||
@objc private func keyboardWillHide(_ notification: Notification?) -> Void {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.systemKeyboardVisible = false
|
||||
@@ -243,6 +280,7 @@ public extension LCSnackBarView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the snackbar's style properties, such as width and corner radius, based on `_style`.
|
||||
private func updateStyle() {
|
||||
switch _style {
|
||||
case .rounded:
|
||||
@@ -256,6 +294,8 @@ public extension LCSnackBarView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Positions the snackbar view within the given superview based on its `_orientation`.
|
||||
/// - Parameter view: The `UIView` that will contain the snackbar.
|
||||
private func positioningView(_ view: UIView) {
|
||||
view
|
||||
.addSubview(self,
|
||||
@@ -286,6 +326,10 @@ public extension LCSnackBarView {
|
||||
height: _height)
|
||||
}
|
||||
|
||||
/// Displays the snackbar with an animation.
|
||||
/// - Parameters:
|
||||
/// - controller: The `UIViewController` on which the snackbar will be presented.
|
||||
/// - completion: A closure to be executed once the presentation animation completes.
|
||||
private func showSnackBar(controller: UIViewController, completion: @escaping (() -> Void)) {
|
||||
if isOpen {
|
||||
closeSnackBar(controller: controller) {
|
||||
@@ -320,6 +364,10 @@ public extension LCSnackBarView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hides the snackbar with an animation.
|
||||
/// - Parameters:
|
||||
/// - controller: The `UIViewController` from which the snackbar is being dismissed.
|
||||
/// - completion: A closure to be executed once the dismissal animation completes.
|
||||
private func closeSnackBar(controller: UIViewController, completion: @escaping (() -> Void)) {
|
||||
let distance = CGFloat(_style == .rounded ? (_orientation == .top ? UIDevice.topNotch : UIDevice.bottomNotch) : 0)
|
||||
layoutIfNeeded()
|
||||
@@ -341,6 +389,8 @@ public extension LCSnackBarView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles tap gestures on the snackbar.
|
||||
/// - Parameter _: The `UITapGestureRecognizer` instance.
|
||||
@objc
|
||||
private func onTapGestureAction(_ : UITapGestureRecognizer) {
|
||||
self.delegate?.snackbar?(didTouchOn: self)
|
||||
@@ -350,6 +400,7 @@ public extension LCSnackBarView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the subviews to the snackbar and sets up their constraints.
|
||||
private func addComponentsAndConstraints() {
|
||||
|
||||
// MARK: - Add Subviews
|
||||
@@ -376,18 +427,29 @@ public extension LCSnackBarView {
|
||||
|
||||
// MARK: - Public methods
|
||||
|
||||
/// Configures the text displayed in the snackbar.
|
||||
/// - Parameter text: The `String` text to display.
|
||||
/// - Returns: The `LCSnackBarView` instance for chaining.
|
||||
@discardableResult
|
||||
func configure(text: String) -> Self {
|
||||
descriptionLabel.text = text
|
||||
return self
|
||||
}
|
||||
|
||||
/// Configures the color of the text in the snackbar.
|
||||
/// - Parameter textColor: The `UIColor` for the text.
|
||||
/// - Returns: The `LCSnackBarView` instance for chaining.
|
||||
@discardableResult
|
||||
func configure(textColor: UIColor) -> Self {
|
||||
descriptionLabel.textColor = textColor
|
||||
return self
|
||||
}
|
||||
|
||||
/// Configures the font and text alignment of the snackbar's text.
|
||||
/// - Parameters:
|
||||
/// - textFont: The `UIFont` for the text.
|
||||
/// - alignment: The `NSTextAlignment` for the text. Defaults to `.center`.
|
||||
/// - Returns: The `LCSnackBarView` instance for chaining.
|
||||
@discardableResult
|
||||
func configure(textFont: UIFont, alignment: NSTextAlignment = .center) -> Self {
|
||||
descriptionLabel.font = textFont
|
||||
@@ -395,6 +457,9 @@ public extension LCSnackBarView {
|
||||
return self
|
||||
}
|
||||
|
||||
/// Configures the background color of the snackbar.
|
||||
/// - Parameter backgroundColor: The `UIColor` for the background.
|
||||
/// - Returns: The `LCSnackBarView` instance for chaining.
|
||||
@discardableResult
|
||||
func configure(backgroundColor: UIColor) -> Self {
|
||||
self.backgroundColor = backgroundColor
|
||||
@@ -402,12 +467,20 @@ public extension LCSnackBarView {
|
||||
return self
|
||||
}
|
||||
|
||||
/// Configures the exhibition duration (timer) of the snackbar.
|
||||
/// - Parameter timer: The `LCSnackBarTimer` value.
|
||||
/// - Returns: The `LCSnackBarView` instance for chaining.
|
||||
@discardableResult
|
||||
func configure(exibition timer: LCSnackBarTimer) -> Self {
|
||||
_timer = timer
|
||||
return self
|
||||
}
|
||||
|
||||
/// Configures an image icon to be displayed before the text in the snackbar.
|
||||
/// - Parameters:
|
||||
/// - icon: The `UIImageView` to use as the icon.
|
||||
/// - withTintColor: An optional `UIColor` to tint the icon. Defaults to `nil`.
|
||||
/// - Returns: The `LCSnackBarView` instance for chaining.
|
||||
@discardableResult
|
||||
func configure(imageIconBefore icon: UIImageView, withTintColor: UIColor? = nil) -> Self {
|
||||
icon.setHeight(size: 24)
|
||||
@@ -428,6 +501,8 @@ public extension LCSnackBarView {
|
||||
return self
|
||||
}
|
||||
|
||||
/// Presents the snackbar on the top-most view controller.
|
||||
/// - Parameter completion: An optional closure to be executed after the presentation.
|
||||
func present(completion: (()->())? = nil) {
|
||||
if isOpen { return }
|
||||
if let controller = LCEssentials.getTopViewController(aboveBars: true) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
//
|
||||
//
|
||||
// Copyright (c) 2024 Loverde Co.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
@@ -21,28 +21,49 @@
|
||||
#if canImport(SwiftUI)
|
||||
import SwiftUI
|
||||
|
||||
/// `LCENavigationState` is an `ObservableObject` that manages the state for `LCENavigationView`.
|
||||
/// It includes published properties for managing button images, text, actions,
|
||||
/// navigation bar visibility, and title/subtitle views.
|
||||
@available(iOS 15, *)
|
||||
class LCENavigationState: ObservableObject {
|
||||
/// The image view for the right button.
|
||||
@Published var rightButtonImage: AnyView? = nil
|
||||
/// The text for the right button.
|
||||
@Published var rightButtonText: Text = Text("")
|
||||
/// The action to perform when the right button is tapped.
|
||||
@Published var rightButtonAction: () -> Void = {}
|
||||
|
||||
/// The image view for the left button.
|
||||
@Published var leftButtonImage: AnyView? = nil
|
||||
/// The text for the left button.
|
||||
@Published var leftButtonText: Text = Text("")
|
||||
/// The action to perform when the left button is tapped.
|
||||
@Published var leftButtonAction: () -> Void = {}
|
||||
|
||||
/// 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("")
|
||||
}
|
||||
|
||||
/// `LCENavigationView` is a SwiftUI `View` that provides a customizable navigation bar.
|
||||
/// It allows setting left and right buttons, a title, and a subtitle.
|
||||
@available(iOS 15, *)
|
||||
public struct LCENavigationView<Content: View>: View {
|
||||
/// The observed state object for the navigation view.
|
||||
@ObservedObject private var state: LCENavigationState
|
||||
|
||||
/// The content view displayed below the navigation bar.
|
||||
let content: Content
|
||||
|
||||
/// Initializes a new `LCENavigationView` instance.
|
||||
/// - Parameters:
|
||||
/// - title: The title view for the navigation bar. Defaults to an empty `Text`.
|
||||
/// - subTitle: The subtitle view for the navigation bar. Defaults to an empty `Text`.
|
||||
/// - content: A `ViewBuilder` that provides the content to be displayed below the navigation bar.
|
||||
public init(
|
||||
title: (any View) = Text(""),
|
||||
subTitle: (any View) = Text(""),
|
||||
@@ -56,6 +77,7 @@ public struct LCENavigationView<Content: View>: View {
|
||||
self.state.subTitle = subTitle
|
||||
}
|
||||
|
||||
/// The body of the `LCENavigationView`.
|
||||
public var body: some View {
|
||||
VStack {
|
||||
if !state.hideNavigationBar {
|
||||
@@ -66,6 +88,7 @@ public struct LCENavigationView<Content: View>: View {
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
|
||||
/// The private `NavigationBarView` that lays out the navigation bar components.
|
||||
private var NavigationBarView: some View {
|
||||
HStack {
|
||||
NavLeftButton
|
||||
@@ -81,6 +104,7 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The private `TitleView` that displays the title and subtitle.
|
||||
private var TitleView: some View {
|
||||
VStack {
|
||||
AnyView(state.title)
|
||||
@@ -90,6 +114,7 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The private `NavLeftButton` view.
|
||||
private var NavLeftButton: some View {
|
||||
Button(action: state.leftButtonAction) {
|
||||
HStack {
|
||||
@@ -101,6 +126,7 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The private `NavRightButton` view.
|
||||
private var NavRightButton: some View {
|
||||
Button(action: state.rightButtonAction) {
|
||||
HStack {
|
||||
@@ -112,6 +138,12 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the configuration for the right button of the navigation bar.
|
||||
/// - Parameters:
|
||||
/// - text: The `Text` to display on the button. Defaults to an empty `Text`.
|
||||
/// - image: An optional `View` to use as the button's icon. Defaults to `nil`.
|
||||
/// - action: The closure to execute when the button is tapped.
|
||||
/// - Returns: The `LCENavigationView` instance for chaining.
|
||||
public func setRightButton(
|
||||
text: Text = Text(""),
|
||||
image: (any View)? = nil,
|
||||
@@ -135,6 +167,12 @@ public struct LCENavigationView<Content: View>: View {
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the configuration for the left button of the navigation bar.
|
||||
/// - Parameters:
|
||||
/// - text: The `Text` to display on the button. Defaults to an empty `Text`.
|
||||
/// - image: An optional `View` to use as the button's icon. Defaults to `nil`.
|
||||
/// - action: The closure to execute when the button is tapped.
|
||||
/// - Returns: The `LCENavigationView` instance for chaining.
|
||||
public func setLeftButton(
|
||||
text: Text = Text(""),
|
||||
image: (any View)? = nil,
|
||||
@@ -158,6 +196,11 @@ public struct LCENavigationView<Content: View>: View {
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the title and optional subtitle for the navigation bar.
|
||||
/// - Parameters:
|
||||
/// - text: The `View` to use as the main title. Defaults to an empty `Text`.
|
||||
/// - subTitle: An optional `View` to use as the subtitle. Defaults to `nil`.
|
||||
/// - Returns: The `LCENavigationView` instance for chaining.
|
||||
public func setTitle(
|
||||
text: (any View) = Text(""),
|
||||
subTitle: (any View)? = nil
|
||||
@@ -167,14 +210,21 @@ public struct LCENavigationView<Content: View>: View {
|
||||
return self
|
||||
}
|
||||
|
||||
/// Controls the visibility of the navigation bar.
|
||||
/// - Parameter hide: A boolean value indicating whether to hide (`true`) or show (`false`) the navigation bar.
|
||||
/// - Returns: The `LCENavigationView` instance for chaining.
|
||||
public func hideNavigationView(_ hide: Bool) -> LCENavigationView {
|
||||
state.hideNavigationBar = hide
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
@@ -183,8 +233,11 @@ extension FormatStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -237,8 +290,11 @@ extension LocalizedStringKey {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
Reference in New Issue
Block a user