35 Commits

Author SHA1 Message Date
Daniel Arantes Loverde
966b439277 Update LCEssentials+API.swift 2026-06-06 10:53:16 -03:00
Daniel Arantes Loverde
a24b79f443 only remote crypto kit 2026-05-24 10:26:25 -03:00
c0eb5f95c6 Merge pull request 'fix: update keyboard notification handling for Swift concurrency' (#7) from bugfix/snackbar_for_iOS26 into main
Reviewed-on: #7
2026-04-07 11:22:02 -03:00
a15d95ed59 Merge branch 'main' into bugfix/snackbar_for_iOS26 2026-04-07 11:21:55 -03:00
Daniel Arantes Loverde
40581d791a fix: update keyboard notification handling for Swift concurrency
Keep snackbar keyboard animations on the main actor and read UIKit keyboard values using the expected notification payload types.

Made-with: Cursor
2026-04-07 11:19:39 -03:00
57ce0dc87a Merge pull request 'iPhone UIDevice names updated' (#6) from feature/LCECryptoKit into main
Reviewed-on: #6
2026-02-11 20:57:39 +00:00
Daniel Arantes Loverde
d06a66226e iPhone UIDevice names updated 2026-02-11 17:56:53 -03:00
336b47ace2 Merge pull request 'Optional' (#5) from feature/LCECryptoKit into main
Reviewed-on: #5
2026-02-06 15:38:09 +00:00
b4a081c4d2 Merge branch 'main' into feature/LCECryptoKit 2026-02-06 15:37:58 +00:00
Daniel Arantes Loverde
57696e3036 Optional 2026-02-06 12:25:34 -03:00
560efd2c8b Merge pull request 'Added more options' (#4) from feature/LCECryptoKit into main
Reviewed-on: https://loverde_git.localhost:3334/Loverde-Company-LTDA/LCEssentials/pulls/4
2025-10-07 23:35:46 +00:00
7e24bdf3d5 Merge branch 'main' into feature/LCECryptoKit 2025-10-07 23:35:37 +00:00
Daniel Arantes Loverde
5df2499c4a Added more options 2025-10-07 20:34:33 -03:00
53ac2c7001 Merge pull request 'Fix LCECryptoKit methods' (#3) from feature/LCECryptoKit into main
Reviewed-on: https://loverde_git.localhost:3334/Loverde-Company-LTDA/LCEssentials/pulls/3
2025-10-07 23:22:04 +00:00
d909709254 Merge branch 'main' into feature/LCECryptoKit 2025-10-07 23:21:59 +00:00
Daniel Arantes Loverde
9a7981b845 Fix LCECryptoKit methods 2025-10-07 20:21:28 -03:00
8f3cbc5024 Merge pull request 'New LCECryptoKit version' (#2) from feature/LCECryptoKit into main
Reviewed-on: https://loverde_git.localhost:3334/Loverde-Company-LTDA/LCEssentials/pulls/2
2025-10-07 18:35:29 +00:00
Daniel Arantes Loverde
fd9eff5226 Merge branch 'main' into feature/LCECryptoKit 2025-10-07 15:34:11 -03:00
Daniel Arantes Loverde
7b86616f92 LCECryptoKit new version 2025-10-07 15:06:10 -03:00
Daniel Arantes Loverde
4f84dfb108 Update README.md 2025-09-02 17:28:28 -03:00
729812d20b Merge pull request 'v1.0.1' (#1) from main into feature/LCECryptoKit
Reviewed-on: https://loverde_git.localhost:3334/Loverde-Company-LTDA/LCEssentials/pulls/1
2025-08-25 19:54:37 +00:00
08519d1aca Merge branch 'feature/LCECryptoKit' into main 2025-08-25 19:54:11 +00:00
Daniel Arantes Loverde
7ac2ccb21f v1.0.1
new repo access updated
2025-08-25 16:52:42 -03:00
Daniel Arantes Loverde
d2ca6e54d2 Improvements 2025-08-15 10:57:14 -03:00
Loverde Co - Git
0eb4f355df Merge branch 'feature/LCECryptoKit' of git/LCEssentials into main
approved
2025-07-29 13:47:32 -03:00
Daniel Arantes Loverde
3e3e181b36 Update Package.swift 2025-07-29 13:46:38 -03:00
Loverde Co - Git
175816dff8 Merge branch 'feature/LCECryptoKit' of git/LCEssentials into main
Approved
2025-07-29 12:00:36 -03:00
Daniel Arantes Loverde
daae48817a LCECripto new methods 2025-07-29 11:59:11 -03:00
Daniel Arantes Loverde
241d69ecc1 LCECryptoKit 2025-07-05 12:34:25 -03:00
Loverde Co - Git
f4fade0442 Merge branch 'feature/documentation' of git/LCEssentials into main 2025-06-23 09:49:56 -03:00
Daniel Arantes Loverde
0910973e9a documentation 2025-06-23 09:48:57 -03:00
Loverde Co - Git
189efd7154 Merge branch 'bugfix/CoreGraphics' of git/LCEssentials into main 2025-04-09 21:20:16 -03:00
Daniel Arantes Loverde
578c69e0f8 Network Fix 2025-04-09 21:18:50 -03:00
Loverde Co - Git
f0f9f1bace Merge branch 'bugfix/CoreGraphics' of git/LCEssentials into main 2025-03-23 00:39:00 -03:00
Daniel Arantes Loverde
6040caa2de Bugfix CoreGraphics 2025-03-23 00:37:11 -03:00
47 changed files with 746 additions and 594 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
.DS_Store .DS_Store
/.build /.build
/build
/Packages /Packages
xcuserdata/ xcuserdata/
DerivedData/ DerivedData/

View File

@@ -22,3 +22,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
Autor: Daniel Arantes Loverde

15
Package.resolved Normal file
View File

@@ -0,0 +1,15 @@
{
"originHash" : "33e7d52ad13cf774717778548edb365d33ff62d766d0049165bc8970f19a23ef",
"pins" : [
{
"identity" : "lcecryptokitbinary",
"kind" : "remoteSourceControl",
"location" : "https://60c260c85d3a2fe840411b0ff98f521b5eca3c56@git.loverde.com.br/Loverde-Company-LTDA/LCECryptoKitBinary.git",
"state" : {
"revision" : "2c5c47cebef40a8adc5557d071a35be405c05e30",
"version" : "1.0.2"
}
}
],
"version" : 3
}

View File

@@ -1,16 +1,43 @@
// swift-tools-version: 6.0 // swift-tools-version: 6.0
import PackageDescription import PackageDescription
import Foundation
let isLocalDevelopment = false //FileManager.default.fileExists(atPath: "../LCECryptoKit/PrivateLib/LCECryptoKitBinary")
let enableCryptoBinary = ProcessInfo.processInfo.environment["LCE_ENABLE_CRYPTO_BINARY"] != "0"
let cryptoPackageURL = isLocalDevelopment
? "../LCECryptoKit/PrivateLib/LCECryptoKitBinary"
: "https://60c260c85d3a2fe840411b0ff98f521b5eca3c56@git.loverde.com.br/Loverde-Company-LTDA/LCECryptoKitBinary.git"
let packageDependencies: [Package.Dependency] = enableCryptoBinary
? [
.package(url: cryptoPackageURL, exact: "1.0.2")
]
: []
let targetDependencies: [Target.Dependency] = enableCryptoBinary
? [
.product(name: "LCECryptoKit", package: "lcecryptokitbinary")
]
: []
let package = Package( let package = Package(
name: "LCEssentials", name: "LCEssentials",
platforms: [
.iOS(.v13),
.macOS(.v10_15),
.tvOS(.v13),
.watchOS(.v6)
],
products: [ products: [
.library( .library(
name: "LCEssentials", name: "LCEssentials",
targets: ["LCEssentials"]), targets: ["LCEssentials"]),
], ],
dependencies: packageDependencies,
targets: [ targets: [
.target( .target(
name: "LCEssentials"), name: "LCEssentials",
dependencies: targetDependencies),
] ]
) )

View File

@@ -17,14 +17,14 @@ Installation
#### Swift Package Manager (SPM) #### Swift Package Manager (SPM)
``` swift ``` swift
dependencies: [ 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: You can also add it via XCode SPM editor with URL:
``` swift ``` swift
http://git.loverde.com.br:3000/git/LCEssentials.git https://git.loverde.com.br/Loverde-Company-LTDA/LCEssentials
``` ```
## Usage example ## Usage example
@@ -59,12 +59,11 @@ And then import `LCEssentials ` wherever you import UIKit or SwiftUI
import LCEssentials import LCEssentials
``` ```
Author:
----
Any question or doubts, please send thru email Any question or doubts, please send thru email
Daniel Arantes Loverde - <daniel@loverde.com.br> Daniel Arantes Loverde - <daniel@loverde.com.br>
[![Alt text](https://loverde.com.br/_signature/loverde_github_mail.gif "My Resume")](https://github.com/loverde-co/resume/) [![Alt text](https://loverde.com.br/_signature/loverde_github_mail.gif "My Resume")](https://github.com/loverde-co/resume/)
[![Alt text](https://loverde.com.br/_signature/loverde_github_mail.gif "Loverde Co. Github")](https://github.com/loverde-co) [![Alt text](https://loverde.com.br/_signature/loverde_github_mail.gif "Loverde Co. Github")](https://github.com/loverde-co)
Autor: Daniel Arantes Loverde

View File

@@ -5,8 +5,10 @@
// Created by iOSDevCenters on 11/12/15. // Created by iOSDevCenters on 11/12/15.
// Copyright © 2016 iOSDevCenters. All rights reserved. // Copyright © 2016 iOSDevCenters. All rights reserved.
// //
#if canImport(UIKit)
import UIKit import UIKit
#if os(iOS) || os(macOS) #endif
#if canImport(UIKit)
import ImageIO import ImageIO
//let jeremyGif = UIImage.gifWithName("jeremy") //let jeremyGif = UIImage.gifWithName("jeremy")

View File

@@ -0,0 +1,96 @@
//
// 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
#if canImport(LCECryptoKit)
import LCECryptoKit
public final class LCECryptoKitManager {
private let hashKey: String
public init() {
self.hashKey = ""
}
public init(privateKey: String){
self.hashKey = privateKey
}
public static func generateKey() -> String {
LCECryptoKit.generateRandomAESKeyString()
}
public func encodeTP(email: String, password: String) -> String? {
return LCECryptoKit.encodeSeed(email: email, password: password)
}
public func decodeOTP(_ otpHash: String) -> String? {
return LCECryptoKit.decodeSeed(otpKey: otpHash)
}
// MARK: Need hashKey to decode
public func encodeOTPWithKey(email: String, password: String) -> String? {
return LCECryptoKit.encodeSeed(email: email, password: password, hashKey: self.hashKey)
}
public func decodeOTPWithKey(_ otpHash: String) -> Bool {
LCECryptoKit.decodeSeed(otpKey: otpHash, hashKey: self.hashKey)
}
}
#else
public final class LCECryptoKitManager {
private let hashKey: String
public init() {
self.hashKey = ""
}
public init(privateKey: String){
self.hashKey = privateKey
}
public static func generateKey() -> String {
""
}
public func encodeTP(email: String, password: String) -> String? {
nil
}
public func decodeOTP(_ otpHash: String) -> String? {
nil
}
public func encodeOTPWithKey(email: String, password: String) -> String? {
nil
}
public func decodeOTPWithKey(_ otpHash: String) -> Bool {
false
}
}
#endif

View File

@@ -28,19 +28,34 @@ import Security
#if canImport(UIKit) #if canImport(UIKit)
import UIKit import UIKit
#endif #endif
#endif
/// A generic `Result` enumeration to represent either a success `Value` or a failure `Error`.
public enum Result<Value, Error: Swift.Error> { public enum Result<Value, Error: Swift.Error> {
/// Indicates a successful operation with an associated `Value`.
case success(Value) case success(Value)
/// Indicates a failed operation with an associated `Error`.
case failure(Error) case failure(Error)
} }
/// Enumeration defining common HTTP methods.
public enum httpMethod: String { public enum httpMethod: String {
/// The POST method.
case post = "POST" case post = "POST"
/// The GET method.
case get = "GET" case get = "GET"
/// The PUT method.
case put = "PUT" case put = "PUT"
/// The PATCH method.
case patch = "PATCH"
/// The DELETE method.
case delete = "DELETE" 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, *) @available(iOS 13.0.0, *)
@MainActor @MainActor
public struct API { public struct API {
@@ -48,20 +63,43 @@ public struct API {
private static var certData: Data? private static var certData: Data?
private static var certPassword: String? 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, static let defaultError = NSError.createErrorWith(code: LCEssentials.DEFAULT_ERROR_CODE,
description: LCEssentials.DEFAULT_ERROR_MSG, description: LCEssentials.DEFAULT_ERROR_MSG,
reasonForError: 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 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]() 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", var defaultHeaders: [String: String] = ["Accept": "application/json",
"Content-Type": "application/json; charset=UTF-8", "Content-Type": "application/json; charset=UTF-8",
"Accept-Encoding": "gzip"] "Accept-Encoding": "gzip"]
/// The shared singleton instance of the `API` struct.
public static let shared = API() public static let shared = API()
private init(){} 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`,`.patch` ).
/// - 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, public func request<T: Codable>(url: String,
params: Any? = nil, params: Any? = nil,
method: httpMethod, method: httpMethod,
@@ -73,8 +111,8 @@ public struct API {
persistConnection: Bool = false) async throws -> T { persistConnection: Bool = false) async throws -> T {
if let urlReq = URL(string: url.replaceURL(params as? [String: Any] ?? [:] )) { if let urlReq = URL(string: url.replaceURL(params as? [String: Any] ?? [:] )) {
var request = URLRequest(url: urlReq, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30) var request = URLRequest(url: urlReq, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: timeoutInterval)
if method == .post || method == .put || method == .delete { if method == .post || method == .put || method == .delete || method == .patch {
if let params = params as? [String: Any], if let params = params as? [String: Any],
let pathFile = params["file"] as? String, let pathFile = params["file"] as? String,
let fileURL = URL(string: pathFile) { let fileURL = URL(string: pathFile) {
@@ -83,7 +121,7 @@ public struct API {
var body = Data() var body = Data()
// Adiciona campos adicionais (se houver) // Add additional fields (if any)
for (key, value) in params where key != "file" { for (key, value) in params where key != "file" {
let stringValue = "\(value)" let stringValue = "\(value)"
body.append("--\(boundary)\r\n".data(using: .utf8)!) body.append("--\(boundary)\r\n".data(using: .utf8)!)
@@ -91,25 +129,36 @@ public struct API {
body.append("\(stringValue)\r\n".data(using: .utf8)!) body.append("\(stringValue)\r\n".data(using: .utf8)!)
} }
// Adiciona o arquivo // Add the file
let fileName = fileURL.lastPathComponent let fileName = fileURL.lastPathComponent
let mimeType = mimeTypeForPath(path: fileName) 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 { do {
let fileData = try Data(contentsOf: fileURL)
body.append("--\(boundary)\r\n".data(using: .utf8)!) 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-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("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
body.append(fileData) let fileDataCopy = Data(fileData)
body.append("\r\n".data(using: .utf8)!) body.append(fileDataCopy)
} catch { let dataUTF8 = "\r\n".data(using: .utf8)!
printError(title: "Upload File", msg: error.localizedDescription) 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)!) body.append("--\(boundary)--\r\n".data(using: .utf8)!)
request.httpBody = body request.httpBody = body
request.setValue("\(body.count)", forHTTPHeaderField: "Content-Length") request.setValue("\(body.count)", forHTTPHeaderField: "Content-Length")
// Logs de depuração // Debug logs
printLog(title: "Boundary", msg: boundary) printLog(title: "Boundary", msg: boundary)
if let bodyString = String(data: body, encoding: .utf8) { if let bodyString = String(data: body, encoding: .utf8) {
printLog(title: "Body Content", msg: bodyString) printLog(title: "Body Content", msg: bodyString)
@@ -131,7 +180,7 @@ public struct API {
request.timeoutInterval = timeoutInterval request.timeoutInterval = timeoutInterval
request.networkServiceType = networkServiceType request.networkServiceType = networkServiceType
// - Put Default Headers togheter with user defined params // - Put Default Headers together with user defined params
if !headers.isEmpty { if !headers.isEmpty {
// - Add it to request // - Add it to request
headers.forEach { (key, value) in headers.forEach { (key, value) in
@@ -187,6 +236,7 @@ public struct API {
} }
if persistConnection { if persistConnection {
printError(title: "INTERNET CONNECTION ERROR", msg: "WILL PERSIST") printError(title: "INTERNET CONNECTION ERROR", msg: "WILL PERSIST")
// Recursive call for persistence
let persist: T = try await self.request( let persist: T = try await self.request(
url: url, url: url,
params: params, params: params,
@@ -221,6 +271,11 @@ public struct API {
throw API.defaultError 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 = "") { public func setupCertificationRequest(certData: Data, password: String = "") {
API.certData = certData API.certData = certData
API.certPassword = password API.certPassword = password
@@ -228,6 +283,8 @@ public struct API {
} }
#if canImport(Security) #if canImport(Security)
/// A custom `URLSessionDelegate` handler for managing URL session challenges,
/// particularly for client and server trust authentication.
@available(iOS 13.0.0, *) @available(iOS 13.0.0, *)
@MainActor @MainActor
private class URLSessionDelegateHandler: NSObject, URLSessionDelegate { private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
@@ -235,24 +292,38 @@ private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
private var certData: Data? private var certData: Data?
private var certPass: String? 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) { init(certData: Data? = nil, password: String? = nil) {
super.init() super.init()
self.certData = certData self.certData = certData
self.certPass = password 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?) { func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
// Carregar o certificado do cliente // Load the client certificate
guard let identity = getIdentity() else { guard let identity = getIdentity() else {
return (URLSession.AuthChallengeDisposition.performDefaultHandling, nil) 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) let credential = URLCredential(identity: identity, certificates: nil, persistence: .forSession)
return (URLSession.AuthChallengeDisposition.useCredential, credential) return (URLSession.AuthChallengeDisposition.useCredential, credential)
} else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
// Validar o certificado do servidor // Validate the server certificate
if let serverTrust = challenge.protectionSpace.serverTrust { if let serverTrust = challenge.protectionSpace.serverTrust {
let serverCredential = URLCredential(trust: serverTrust) let serverCredential = URLCredential(trust: serverTrust)
return (URLSession.AuthChallengeDisposition.useCredential, serverCredential) return (URLSession.AuthChallengeDisposition.useCredential, serverCredential)
@@ -264,13 +335,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? { private func getIdentity() -> SecIdentity? {
guard let certData = self.certData else { return nil } 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 ?? ""] let options: [String: Any] = [kSecImportExportPassphrase as String: self.certPass ?? ""]
var items: CFArray? 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) let status = SecPKCS12Import(certData as CFData, options as CFDictionary, &items)
if status == errSecSuccess, if status == errSecSuccess,
@@ -285,6 +359,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 { private func isPKCS12(data: Data, password: String) -> Bool {
let options: [String: Any] = [kSecImportExportPassphrase as String: password] let options: [String: Any] = [kSecImportExportPassphrase as String: password]
@@ -297,6 +377,7 @@ private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
#endif #endif
extension Error { extension Error {
/// Returns the status code of the error, if available.
public var statusCode: Int { public var statusCode: Int {
get{ get{
return self._code return self._code
@@ -307,6 +388,11 @@ extension Error {
@available(iOS 13.0.0, *) @available(iOS 13.0.0, *)
extension API { 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) { fileprivate static func requestLOG(method: httpMethod, request: URLRequest) {
print("\n<========================= 🟠 INTERNET CONNECTION - REQUEST =========================>") print("\n<========================= 🟠 INTERNET CONNECTION - REQUEST =========================>")
@@ -325,6 +411,14 @@ extension API {
print("<======================================================================================>") 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?) { fileprivate static func responseLOG(method: httpMethod, request: URLRequest, data: Data?, statusCode: Int, error: Error?) {
/// ///
let icon = error != nil ? "🔴" : "🟢" let icon = error != nil ? "🔴" : "🟢"
@@ -384,11 +478,15 @@ extension API {
print("<======================================================================================>") 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 { func mimeTypeForPath(path: String) -> String {
let url = URL(fileURLWithPath: path) let url = URL(fileURLWithPath: path)
let pathExtension = url.pathExtension.lowercased() 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] = [ let mimeTypes: [String: String] = [
"jpg": "image/jpeg", "jpg": "image/jpeg",
"jpeg": "image/jpeg", "jpeg": "image/jpeg",
@@ -412,7 +510,7 @@ extension API {
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation" "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" return mimeTypes[pathExtension] ?? "application/octet-stream"
} }
} }

View File

@@ -21,8 +21,12 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
#if canImport(AVFoundation)
import AVFoundation import AVFoundation
#endif
#if os(watchOS) #if os(watchOS)
import WatchKit import WatchKit
#endif #endif
@@ -155,7 +159,7 @@ public struct LCEssentials {
diskPath: "file_cache" diskPath: "file_cache"
) )
#if os(iOS) || os(macOS) #if canImport(UIKit)
/// Extract the file name from the file path /// Extract the file name from the file path
/// ///
/// - Parameter filePath: Full file path in bundle /// - Parameter filePath: Full file path in bundle
@@ -266,7 +270,11 @@ public struct LCEssentials {
/// - LoverdeCo: Check if app is running in debug mode. /// - LoverdeCo: Check if app is running in debug mode.
@MainActor @MainActor
public static var isInDebuggingMode: Bool { public static var isInDebuggingMode: Bool {
#if canImport(UIKit)
return UIApplication.inferredEnvironment == .debug return UIApplication.inferredEnvironment == .debug
#else
return false
#endif
} }
#if !os(macOS) #if !os(macOS)
@@ -416,7 +424,7 @@ public struct LCEssentials {
// MARK: - Methods // MARK: - Methods
public extension LCEssentials { public extension LCEssentials {
#if os(iOS) || os(macOS) #if canImport(UIKit)
/// - LoverdeCo: Share link with message /// - LoverdeCo: Share link with message
/// ///
/// - Parameters: /// - Parameters:

View File

@@ -20,7 +20,9 @@
// THE SOFTWARE. // THE SOFTWARE.
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
@objc public protocol LCESingletonDelegate: AnyObject { @objc public protocol LCESingletonDelegate: AnyObject {
@objc optional func singleton(object: Any?, withData: Any) @objc optional func singleton(object: Any?, withData: Any)

View File

@@ -21,7 +21,9 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
// MARK: - Methods (Equatable) // MARK: - Methods (Equatable)
public extension Array where Element: Equatable { public extension Array where Element: Equatable {
@@ -148,6 +150,7 @@ public extension Array where Element: Equatable {
} }
} }
#if canImport(UIKit)
extension Array where Element == NSLayoutConstraint { extension Array where Element == NSLayoutConstraint {
@MainActor @MainActor
@@ -169,3 +172,5 @@ extension Array where Element == NSLayoutConstraint {
} }
} }
} }
#endif

View File

@@ -21,9 +21,11 @@
import Foundation import Foundation
#if canImport(CoreGraphics)
import CoreGraphics
#if canImport(SwiftUI) #if canImport(SwiftUI)
extension CGPoint: Hashable { extension CGPoint: @retroactive Hashable {
// Implementação manual de Hashable // Implementação manual de Hashable
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
hasher.combine(x) hasher.combine(x)
@@ -35,3 +37,4 @@ extension CGPoint: Hashable {
} }
} }
#endif #endif
#endif

View File

@@ -30,7 +30,7 @@ public extension CGRect {
var center: CGPoint { CGPoint(x: midX, y: midY) } var center: CGPoint { CGPoint(x: midX, y: midY) }
} }
#if canImport(SwiftUI) #if canImport(SwiftUI)
extension CGRect: Hashable { extension CGRect: @retroactive Hashable {
// Implementação manual de Hashable // Implementação manual de Hashable
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
hasher.combine(origin) hasher.combine(origin)

View File

@@ -21,8 +21,12 @@
import Foundation import Foundation
#if canImport(CryptoKit)
import CryptoKit import CryptoKit
#endif
#if canImport(CommonCrypto)
import CommonCrypto import CommonCrypto
#endif
public extension Data { public extension Data {
@@ -71,17 +75,25 @@ public extension Data {
@available(iOS 13.0, *) @available(iOS 13.0, *)
func HMACSHA512(key: Data) -> Data { func HMACSHA512(key: Data) -> Data {
#if canImport(CryptoKit)
var hmac = HMAC<SHA512>.init(key: SymmetricKey(data: key)) var hmac = HMAC<SHA512>.init(key: SymmetricKey(data: key))
hmac.update(data: self) hmac.update(data: self)
return Data(hmac.finalize()) return Data(hmac.finalize())
#else
return Data()
#endif
} }
func SHA512() -> Data { func SHA512() -> Data {
#if canImport(CommonCrypto)
var digest = [UInt8](repeating: 0, count: Int(CC_SHA512_DIGEST_LENGTH)) var digest = [UInt8](repeating: 0, count: Int(CC_SHA512_DIGEST_LENGTH))
self.withUnsafeBytes { self.withUnsafeBytes {
_ = CC_SHA512($0.baseAddress, CC_LONG(self.count), &digest) _ = CC_SHA512($0.baseAddress, CC_LONG(self.count), &digest)
} }
return Data(digest) return Data(digest)
#else
return Data()
#endif
} }
func XOR(with other: Data) -> Data { func XOR(with other: Data) -> Data {
@@ -89,11 +101,15 @@ public extension Data {
} }
func SHA256() -> Data { func SHA256() -> Data {
#if canImport(CommonCrypto)
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
self.withUnsafeBytes { self.withUnsafeBytes {
_ = CC_SHA256($0.baseAddress, CC_LONG(self.count), &digest) _ = CC_SHA256($0.baseAddress, CC_LONG(self.count), &digest)
} }
return Data(digest) return Data(digest)
#else
return Data()
#endif
} }
func object<T: Codable>() -> T? { func object<T: Codable>() -> T? {
@@ -124,9 +140,13 @@ public extension Data {
///print("md5Hex: \(md5Hex)") ///print("md5Hex: \(md5Hex)")
@available(iOS 13.0, *) @available(iOS 13.0, *)
static func MD5(string: String) -> Data { static func MD5(string: String) -> Data {
#if canImport(CryptoKit)
let messageData = string.data(using: .utf8)! let messageData = string.data(using: .utf8)!
let digestData = Insecure.MD5.hash (data: messageData) let digestData = Insecure.MD5.hash (data: messageData)
let digestHex = String(digestData.map { String(format: "%02hhx", $0) }.joined().prefix(32)) let digestHex = String(digestData.map { String(format: "%02hhx", $0) }.joined().prefix(32))
return Data(digestHex.utf8) return Data(digestHex.utf8)
#else
return Data()
#endif
} }
} }

View File

@@ -21,7 +21,9 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
public extension Dictionary { public extension Dictionary {

View File

@@ -58,7 +58,8 @@ extension JSONDecoder {
let msg = "Type mismatch for type '\(type)' \(T.self) Object: \(context.debugDescription)" let msg = "Type mismatch for type '\(type)' \(T.self) Object: \(context.debugDescription)"
error = NSError.createErrorWith(code: 0, description: msg, reasonForError: msg) error = NSError.createErrorWith(code: 0, description: msg, reasonForError: msg)
} catch let DecodingError.valueNotFound(value, context) { } 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) error = NSError.createErrorWith(code: 0, description: msg, reasonForError: msg)
} catch { } catch {
throw error throw error

View File

@@ -21,7 +21,9 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
public extension FileManager { public extension FileManager {
@@ -54,7 +56,7 @@ public extension FileManager {
} }
} }
#if os(iOS) || os(macOS) #if canImport(UIKit)
func saveImageToDirectory( _ imageWithPath : String, imagem : UIImage ) -> Bool { func saveImageToDirectory( _ imageWithPath : String, imagem : UIImage ) -> Bool {
let data = imagem.pngData() let data = imagem.pngData()

View File

@@ -21,8 +21,11 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
#if canImport(UIKit)
extension NSLayoutConstraint { extension NSLayoutConstraint {
func constraintWithMultiplier(_ multiplier: CGFloat) -> NSLayoutConstraint { func constraintWithMultiplier(_ multiplier: CGFloat) -> NSLayoutConstraint {
@@ -72,3 +75,4 @@ extension NSLayoutConstraint {
return false return false
} }
} }
#endif

View File

@@ -21,8 +21,10 @@
import Foundation import Foundation
#if os(iOS) || os(macOS) #if canImport(UIKit)
#if canImport(UIKit)
import UIKit import UIKit
#endif
public extension NSMutableAttributedString { public extension NSMutableAttributedString {
@discardableResult func customize(_ text: String, @discardableResult func customize(_ text: String,

View File

@@ -20,7 +20,9 @@
// THE SOFTWARE. // THE SOFTWARE.
#if canImport(UIKit)
import UIKit import UIKit
#endif
public extension Optional { public extension Optional {
/// Get self of default value (if self is nil). /// Get self of default value (if self is nil).

View File

@@ -21,7 +21,9 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
#if canImport(CommonCrypto) #if canImport(CommonCrypto)
import CommonCrypto import CommonCrypto
#endif #endif
@@ -48,10 +50,14 @@ public extension String {
} }
var convertToHTML: NSAttributedString? { var convertToHTML: NSAttributedString? {
#if canImport(UIKit)
return convertHtmlToAttributedStringWithCSS(font: nil, return convertHtmlToAttributedStringWithCSS(font: nil,
csscolor: "", csscolor: "",
lineheight: 0, lineheight: 0,
csstextalign: "") csstextalign: "")
#else
return convertHtmlToNSAttributedString
#endif
} }
/// Check if string is a valid URL. /// Check if string is a valid URL.
@@ -646,7 +652,7 @@ public extension String {
return strDate?.date(withCurrFormatt: newFormatt, localeIdentifier: localeIdentifier, timeZone: timeZone) return strDate?.date(withCurrFormatt: newFormatt, localeIdentifier: localeIdentifier, timeZone: timeZone)
} }
#if os(iOS) || os(macOS) #if canImport(UIKit)
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat { func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude) let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil) let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
@@ -757,7 +763,7 @@ public extension String {
} }
return self return self
} }
#if canImport(UIKit)
/// Converte String para HTML com CSS. /// Converte String para HTML com CSS.
/// ///
/// - Parameters: /// - Parameters:
@@ -801,6 +807,7 @@ public extension String {
return nil return nil
} }
} }
#endif
/// Float value from string (if applicable). /// Float value from string (if applicable).
/// ///

View File

@@ -19,8 +19,10 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE. // THE SOFTWARE.
#if canImport(UIKit) && os(iOS) || os(macOS) #if canImport(UIKit)
#if canImport(UIKit)
import UIKit import UIKit
#endif
public extension UIApplication { public extension UIApplication {
/// Application running environment. /// Application running environment.

View File

@@ -21,9 +21,11 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
#if os(iOS) || os(macOS) #if canImport(UIKit)
// MARK: - Properties // MARK: - Properties
public extension UIButton { public extension UIButton {

View File

@@ -21,8 +21,10 @@
import Foundation import Foundation
#if os(iOS) || os(macOS) #if canImport(UIKit)
#if canImport(UIKit)
import UIKit import UIKit
#endif
public extension UICollectionView { public extension UICollectionView {

View File

@@ -21,9 +21,11 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
#if os(iOS) || os(macOS) #if canImport(UIKit)
public extension UIColor { public extension UIColor {
var redValue: CGFloat{ return CIColor(color: self).red } var redValue: CGFloat{ return CIColor(color: self).red }

View File

@@ -21,9 +21,11 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
#if os(iOS) || os(macOS) #if canImport(UIKit)
public extension UIDevice { public extension UIDevice {
static var topNotch: CGFloat { static var topNotch: CGFloat {
@@ -64,59 +66,104 @@ public extension UIDevice {
#if os(iOS) #if os(iOS)
switch identifier { switch identifier {
case "iPod5,1": return "iPod touch (5th generation)" case "iPod5,1": return "iPod touch (5th generation)"
case "iPod7,1": return "iPod touch (6th generation)" case "iPod7,1": return "iPod touch (6th generation)"
case "iPod9,1": return "iPod touch (7th generation)" case "iPod9,1": return "iPod touch (7th generation)"
case "iPhone3,1", "iPhone3,2", "iPhone3,3": return "iPhone 4" case "iPhone3,1", "iPhone3,2", "iPhone3,3": return "iPhone 4"
case "iPhone4,1": return "iPhone 4s" case "iPhone4,1": return "iPhone 4s"
case "iPhone5,1", "iPhone5,2": return "iPhone 5" case "iPhone5,1", "iPhone5,2": return "iPhone 5"
case "iPhone5,3", "iPhone5,4": return "iPhone 5c" case "iPhone5,3", "iPhone5,4": return "iPhone 5c"
case "iPhone6,1", "iPhone6,2": return "iPhone 5s" case "iPhone6,1", "iPhone6,2": return "iPhone 5s"
case "iPhone7,2": return "iPhone 6" case "iPhone7,2": return "iPhone 6"
case "iPhone7,1": return "iPhone 6 Plus" case "iPhone7,1": return "iPhone 6 Plus"
case "iPhone8,1": return "iPhone 6s" case "iPhone8,1": return "iPhone 6s"
case "iPhone8,2": return "iPhone 6s Plus" case "iPhone8,2": return "iPhone 6s Plus"
case "iPhone9,1", "iPhone9,3": return "iPhone 7" case "iPhone9,1", "iPhone9,3": return "iPhone 7"
case "iPhone9,2", "iPhone9,4": return "iPhone 7 Plus" case "iPhone9,2", "iPhone9,4": return "iPhone 7 Plus"
case "iPhone8,4", "iPhone12,8": return "iPhone SE" case "iPhone10,1", "iPhone10,4": return "iPhone 8"
case "iPhone10,1", "iPhone10,4": return "iPhone 8" case "iPhone10,2", "iPhone10,5": return "iPhone 8 Plus"
case "iPhone10,2", "iPhone10,5": return "iPhone 8 Plus" case "iPhone10,3", "iPhone10,6": return "iPhone X"
case "iPhone10,3", "iPhone10,6": return "iPhone X" case "iPhone11,2": return "iPhone XS"
case "iPhone11,2": return "iPhone XS" case "iPhone11,4", "iPhone11,6": return "iPhone XS Max"
case "iPhone11,4", "iPhone11,6": return "iPhone XS Max" case "iPhone11,8": return "iPhone XR"
case "iPhone11,8": return "iPhone XR" case "iPhone12,1": return "iPhone 11"
case "iPhone12,1": return "iPhone 11" case "iPhone12,3": return "iPhone 11 Pro"
case "iPhone12,3": return "iPhone 11 Pro" case "iPhone12,5": return "iPhone 11 Pro Max"
case "iPhone12,5": return "iPhone 11 Pro Max" case "iPhone13,1": return "iPhone 12 mini"
case "iPhone13,1": return "iPhone 12 mini" case "iPhone13,2": return "iPhone 12"
case "iPhone13,2": return "iPhone 12" case "iPhone13,3": return "iPhone 12 Pro"
case "iPhone13,3": return "iPhone 12 Pro" case "iPhone13,4": return "iPhone 12 Pro Max"
case "iPhone13,4": return "iPhone 12 Pro Max" case "iPhone14,4": return "iPhone 13 mini"
case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4":return "iPad 2" case "iPhone14,5": return "iPhone 13"
case "iPad3,1", "iPad3,2", "iPad3,3": return "iPad 3" case "iPhone14,2": return "iPhone 13 Pro"
case "iPad3,4", "iPad3,5", "iPad3,6": return "iPad 4" case "iPhone14,3": return "iPhone 13 Pro Max"
case "iPad4,1", "iPad4,2", "iPad4,3": return "iPad Air" case "iPhone14,7": return "iPhone 14"
case "iPad5,3", "iPad5,4": return "iPad Air 2" case "iPhone14,8": return "iPhone 14 Plus"
case "iPad6,11", "iPad6,12": return "iPad 5" case "iPhone15,2": return "iPhone 14 Pro"
case "iPad7,5", "iPad7,6": return "iPad 6" case "iPhone15,3": return "iPhone 14 Pro Max"
case "iPad7,11", "iPad7,12": return "iPad 7" case "iPhone15,4": return "iPhone 15"
case "iPad11,4", "iPad11,5": return "iPad Air (3rd generation)" case "iPhone15,5": return "iPhone 15 Plus"
case "iPad2,5", "iPad2,6", "iPad2,7": return "iPad Mini" case "iPhone16,1": return "iPhone 15 Pro"
case "iPad4,4", "iPad4,5", "iPad4,6": return "iPad Mini 2" case "iPhone16,2": return "iPhone 15 Pro Max"
case "iPad4,7", "iPad4,8", "iPad4,9": return "iPad Mini 3" case "iPhone17,3": return "iPhone 16"
case "iPad5,1", "iPad5,2": return "iPad Mini 4" case "iPhone17,4": return "iPhone 16 Plus"
case "iPad11,1", "iPad11,2": return "iPad Mini 5" case "iPhone17,1": return "iPhone 16 Pro"
case "iPad6,3", "iPad6,4": return "iPad Pro (9.7-inch)" case "iPhone17,2": return "iPhone 16 Pro Max"
case "iPad6,7", "iPad6,8": return "iPad Pro (12.9-inch)" case "iPhone17,5": return "iPhone 16e"
case "iPad7,1", "iPad7,2": return "iPad Pro (12.9-inch) (2nd generation)" case "iPhone18,3": return "iPhone 17"
case "iPad7,3", "iPad7,4": return "iPad Pro (10.5-inch)" case "iPhone18,4": return "iPhone Air"
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4":return "iPad Pro (11-inch)" case "iPhone18,1": return "iPhone 17 Pro"
case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8":return "iPad Pro (12.9-inch) (3rd generation)" case "iPhone18,2": return "iPhone 17 Pro Max"
case "AppleTV5,3": return "Apple TV" case "iPhone8,4": return "iPhone SE"
case "AppleTV6,2": return "Apple TV 4K" case "iPhone12,8": return "iPhone SE (2nd generation)"
case "AudioAccessory1,1": return "HomePod" case "iPhone14,6": return "iPhone SE (3rd generation)"
case "i386", "x86_64": return "Simulator \(identifier)" case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return "iPad 2"
default: return identifier case "iPad3,1", "iPad3,2", "iPad3,3": return "iPad (3rd generation)"
case "iPad3,4", "iPad3,5", "iPad3,6": return "iPad (4th generation)"
case "iPad6,11", "iPad6,12": return "iPad (5th generation)"
case "iPad7,5", "iPad7,6": return "iPad (6th generation)"
case "iPad7,11", "iPad7,12": return "iPad (7th generation)"
case "iPad11,6", "iPad11,7": return "iPad (8th generation)"
case "iPad12,1", "iPad12,2": return "iPad (9th generation)"
case "iPad13,18", "iPad13,19": return "iPad (10th generation)"
case "iPad15,7", "iPad15,8": return "iPad (11th generation)"
case "iPad4,1", "iPad4,2", "iPad4,3": return "iPad Air"
case "iPad5,3", "iPad5,4": return "iPad Air 2"
case "iPad11,3", "iPad11,4": return "iPad Air (3rd generation)"
case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)"
case "iPad13,16", "iPad13,17": return "iPad Air (5th generation)"
case "iPad14,8", "iPad14,9": return "iPad Air (11-inch) (M2)"
case "iPad14,10", "iPad14,11": return "iPad Air (13-inch) (M2)"
case "iPad15,3", "iPad15,4": return "iPad Air (11-inch) (M3)"
case "iPad15,5", "iPad15,6": return "iPad Air (13-inch) (M3)"
case "iPad2,5", "iPad2,6", "iPad2,7": return "iPad mini"
case "iPad4,4", "iPad4,5", "iPad4,6": return "iPad mini 2"
case "iPad4,7", "iPad4,8", "iPad4,9": return "iPad mini 3"
case "iPad5,1", "iPad5,2": return "iPad mini 4"
case "iPad11,1", "iPad11,2": return "iPad mini (5th generation)"
case "iPad14,1", "iPad14,2": return "iPad mini (6th generation)"
case "iPad16,1", "iPad16,2": return "iPad mini (A17 Pro)"
case "iPad6,3", "iPad6,4": return "iPad Pro (9.7-inch)"
case "iPad7,3", "iPad7,4": return "iPad Pro (10.5-inch)"
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return "iPad Pro (11-inch) (1st generation)"
case "iPad8,9", "iPad8,10": return "iPad Pro (11-inch) (2nd generation)"
case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro (11-inch) (3rd generation)"
case "iPad14,3", "iPad14,4": return "iPad Pro (11-inch) (4th generation)"
case "iPad16,3", "iPad16,4": return "iPad Pro (11-inch) (M4)"
case "iPad17,1", "iPad17,2": return "iPad Pro (11-inch) (M5)"
case "iPad6,7", "iPad6,8": return "iPad Pro (12.9-inch) (1st generation)"
case "iPad7,1", "iPad7,2": return "iPad Pro (12.9-inch) (2nd generation)"
case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return "iPad Pro (12.9-inch) (3rd generation)"
case "iPad8,11", "iPad8,12": return "iPad Pro (12.9-inch) (4th generation)"
case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11":return "iPad Pro (12.9-inch) (5th generation)"
case "iPad14,5", "iPad14,6": return "iPad Pro (12.9-inch) (6th generation)"
case "iPad16,5", "iPad16,6": return "iPad Pro (13-inch) (M4)"
case "iPad17,3", "iPad17,4": return "iPad Pro (13-inch) (M5)"
case "AppleTV5,3": return "Apple TV"
case "AppleTV6,2": return "Apple TV 4K"
case "AudioAccessory1,1": return "HomePod"
case "AudioAccessory5,1": return "HomePod mini"
default: return identifier
} }
#endif #endif
} }

View File

@@ -21,9 +21,11 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
#if os(iOS) || os(macOS) #if canImport(UIKit)
public extension UIImage { public extension UIImage {
//Extension Required by RoundedButton to create UIImage from UIColor //Extension Required by RoundedButton to create UIImage from UIColor
func imageWithColor(color: UIColor) -> UIImage { func imageWithColor(color: UIColor) -> UIImage {

View File

@@ -21,9 +21,11 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
#if os(iOS) || os(macOS) #if canImport(UIKit)
@MainActor @MainActor
public extension UIImageView { public extension UIImageView {

View File

@@ -21,9 +21,11 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
#if os(iOS) || os(macOS) #if canImport(UIKit)
public extension UILabel { public extension UILabel {
func lineNumbers() -> Int{ func lineNumbers() -> Int{

View File

@@ -21,8 +21,10 @@
import Foundation import Foundation
#if os(iOS) || os(macOS) #if canImport(UIKit)
#if canImport(UIKit)
import UIKit import UIKit
#endif
import QuartzCore import QuartzCore
public extension UINavigationController { public extension UINavigationController {

View File

@@ -21,8 +21,10 @@
import Foundation import Foundation
#if os(iOS) || os(macOS) #if canImport(UIKit)
#if canImport(UIKit)
import UIKit import UIKit
#endif
public extension UIResponder { public extension UIResponder {

View File

@@ -21,7 +21,9 @@
#if canImport(UIKit) && !os(watchOS) #if canImport(UIKit) && !os(watchOS)
#if canImport(UIKit)
import UIKit import UIKit
#endif
// MARK: - Methods // MARK: - Methods
public extension UIScrollView { public extension UIScrollView {

View File

@@ -21,7 +21,9 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
#if os(iOS) #if os(iOS)
// MARK: - Initializers // MARK: - Initializers

View File

@@ -21,8 +21,10 @@
import Foundation import Foundation
#if os(iOS) || os(macOS) #if canImport(UIKit)
#if canImport(UIKit)
import UIKit import UIKit
#endif
public class CustomTabBadge: UILabel { public class CustomTabBadge: UILabel {

View File

@@ -21,9 +21,11 @@
import Foundation import Foundation
#if canImport(UIKit)
import UIKit import UIKit
#endif
#if os(iOS) || os(macOS) #if canImport(UIKit)
//MARK: - UITableView Animation Cell //MARK: - UITableView Animation Cell
public typealias UITableViewCellAnimation = (UITableViewCell, IndexPath, UITableView) -> Void public typealias UITableViewCellAnimation = (UITableViewCell, IndexPath, UITableView) -> Void

View File

@@ -21,8 +21,10 @@
import Foundation import Foundation
#if canImport(UIKit) && os(iOS) || os(macOS) #if canImport(UIKit)
#if canImport(UIKit)
import UIKit import UIKit
#endif
public extension UITapGestureRecognizer { public extension UITapGestureRecognizer {

View File

@@ -21,8 +21,10 @@
import Foundation import Foundation
#if os(iOS) || os(macOS) #if canImport(UIKit)
#if canImport(UIKit)
import UIKit import UIKit
#endif
public extension UITextField { public extension UITextField {

View File

@@ -21,8 +21,10 @@
import Foundation import Foundation
#if os(iOS) || os(macOS) #if canImport(UIKit)
#if canImport(UIKit)
import UIKit import UIKit
#endif
typealias GradientPoints = (startPoint: CGPoint, endPoint: CGPoint) typealias GradientPoints = (startPoint: CGPoint, endPoint: CGPoint)

View File

@@ -22,8 +22,10 @@
import Foundation import Foundation
#if os(iOS) || os(macOS) #if canImport(UIKit)
#if canImport(UIKit)
import UIKit import UIKit
#endif
import QuartzCore import QuartzCore
public enum ToastPosition { public enum ToastPosition {

View File

@@ -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

View File

@@ -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

View File

@@ -20,22 +20,36 @@
// THE SOFTWARE. // THE SOFTWARE.
#if os(iOS) || os(macOS) #if canImport(UIKit)
#if canImport(UIKit)
import UIKit import UIKit
#endif
import AVFoundation import AVFoundation
import Photos import Photos
/// A protocol that defines the methods an image picker delegate should implement.
public protocol ImagePickerControllerDelegate: AnyObject { 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?) 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 { public class ImagePickerController: UIViewController, UINavigationControllerDelegate {
private var isAlertOpen: Bool = false private var isAlertOpen: Bool = false
private var imagePickerController: UIImagePickerController = UIImagePickerController() private var imagePickerController: UIImagePickerController = UIImagePickerController()
/// The delegate for the `ImagePickerController`, which will receive callbacks when an image is selected.
public weak var delegate: ImagePickerControllerDelegate? 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 public var isEditable: Bool = false
/// Initializes a new `ImagePickerController` instance.
public init() { public init() {
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
@@ -52,7 +66,10 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
self.imagePickerController.mediaTypes = ["public.image", "public.movie"] 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(){ public func openImagePicker(){
var cameraPerm: Bool = false var cameraPerm: Bool = false
var albumPerm: Bool = false var albumPerm: Bool = false
@@ -85,6 +102,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){ private func openAlerts(forCamera:Bool = true, forAlbum:Bool = true){
let alert = UIAlertController(title: "Choose an option", message: nil, preferredStyle: .actionSheet) let alert = UIAlertController(title: "Choose an option", message: nil, preferredStyle: .actionSheet)
if forCamera { if forCamera {
@@ -117,6 +138,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(){ private func openCameraDevice(){
if(UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera)){ if(UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera)){
self.imagePickerController.sourceType = UIImagePickerController.SourceType.camera self.imagePickerController.sourceType = UIImagePickerController.SourceType.camera
@@ -130,12 +154,14 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
} }
} }
/// Opens the photo library using `UIImagePickerController`.
private func openAlbumDevice(){ private func openAlbumDevice(){
self.imagePickerController.sourceType = UIImagePickerController.SourceType.photoLibrary self.imagePickerController.sourceType = UIImagePickerController.SourceType.photoLibrary
self.modalPresentationStyle = .fullScreen self.modalPresentationStyle = .fullScreen
self.present(self.imagePickerController, animated: true, completion: nil) self.present(self.imagePickerController, animated: true, completion: nil)
} }
/// Requests photo library access and if denied, guides the user to app settings.
private func openAppSettingsPhoto(){ private func openAppSettingsPhoto(){
PHPhotoLibrary.requestAuthorization({status in PHPhotoLibrary.requestAuthorization({status in
if status == .authorized{ if status == .authorized{
@@ -160,6 +186,7 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
}) })
} }
/// Requests camera access and if denied, guides the user to app settings.
private func openAppSettingsCamera(){ private func openAppSettingsCamera(){
AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (granted: Bool) -> Void in AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (granted: Bool) -> Void in
if granted == true { if granted == true {
@@ -189,6 +216,8 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
extension ImagePickerController: UIImagePickerControllerDelegate { 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) { @objc public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
self.delegate?.imagePicker(didSelect: nil) self.delegate?.imagePicker(didSelect: nil)
picker.dismiss(animated: true, completion: { picker.dismiss(animated: true, completion: {
@@ -196,6 +225,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, @objc public func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage { if let image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {

View File

@@ -19,16 +19,30 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE. // THE SOFTWARE.
#if canImport(UIKit)
import UIKit import UIKit
#endif
#if os(iOS) || os(macOS) #if canImport(UIKit)
/// A protocol for delegates of `ImageZoomController` to provide callbacks for zoom and close events.
@objc public protocol ImageZoomControllerDelegate { @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?) @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?) @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 { public class ImageZoomController: UIViewController {
/// A private lazy `UIView` that serves as a black background with partial opacity.
fileprivate lazy var blackView: UIView = { fileprivate lazy var blackView: UIView = {
$0.isOpaque = true $0.isOpaque = true
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
@@ -37,6 +51,7 @@ public class ImageZoomController: UIViewController {
return $0 return $0
}(UIView()) }(UIView())
/// A private lazy `UIScrollView` to enable zooming and panning of the image.
fileprivate lazy var scrollView: UIScrollView = { fileprivate lazy var scrollView: UIScrollView = {
$0.isOpaque = true $0.isOpaque = true
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
@@ -44,6 +59,7 @@ public class ImageZoomController: UIViewController {
return $0 return $0
}(UIScrollView()) }(UIScrollView())
/// A private lazy `UIButton` to close the image zoom controller.
fileprivate lazy var closeButton: UIButton = { fileprivate lazy var closeButton: UIButton = {
$0.isOpaque = true $0.isOpaque = true
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
@@ -55,6 +71,7 @@ public class ImageZoomController: UIViewController {
return $0 return $0
}(UIButton(type: .custom)) }(UIButton(type: .custom))
/// The `UIImageView` that displays the image.
lazy var imageView: UIImageView = { lazy var imageView: UIImageView = {
$0.isOpaque = true $0.isOpaque = true
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
@@ -62,16 +79,24 @@ public class ImageZoomController: UIViewController {
return $0 return $0
}(UIImageView()) }(UIImageView())
/// The minimum zoom scale allowed for the image. Defaults to `1.0`.
public var minimumZoomScale: CGFloat = 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 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 public var addGestureToDismiss: Bool = true
/// The delegate for the `ImageZoomController`.
public weak var delegate: ImageZoomControllerDelegate? public weak var delegate: ImageZoomControllerDelegate?
private var minimumVelocityToHide: CGFloat = 1500 private var minimumVelocityToHide: CGFloat = 1500
private var minimumScreenRatioToHide: CGFloat = 0.5 private var minimumScreenRatioToHide: CGFloat = 0.5
private var animationDuration: TimeInterval = 0.2 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) { public init(_ withImage: UIImage) {
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
@@ -101,6 +126,7 @@ public class ImageZoomController: UIViewController {
} }
} }
/// Adds the subviews and sets up their constraints.
fileprivate func addComponentsAndConstraints() { fileprivate func addComponentsAndConstraints() {
scrollView.addSubview(imageView, translatesAutoresizingMaskIntoConstraints: false) scrollView.addSubview(imageView, translatesAutoresizingMaskIntoConstraints: false)
@@ -124,6 +150,8 @@ public class ImageZoomController: UIViewController {
.setHeight(size: 50.0) .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) { public func present(completion: (()->())? = nil) {
guard let viewController = LCEssentials.getTopViewController(aboveBars: true) else { guard let viewController = LCEssentials.getTopViewController(aboveBars: true) else {
fatalError("Ops! Look like it doesnt have a ViewController") fatalError("Ops! Look like it doesnt have a ViewController")
@@ -135,21 +163,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) { public func dismiss(completion: (()->())? = nil) {
self.dismiss(animated: true) { self.dismiss(animated: true) {
completion?() completion?()
} }
} }
/// Action method for the close button.
/// Notifies the delegate that the controller is closing and then dismisses itself.
@objc private func close(){ @objc private func close(){
delegate?.imageZoomController?(controller: self, didClose: self.imageView.image) delegate?.imageZoomController?(controller: self, didClose: self.imageView.image)
self.dismiss() 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) { private func slideViewVerticallyTo(_ y: CGFloat) {
self.view.frame.origin = CGPoint(x: 0, y: y) 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) { @objc private func onPan(_ panGesture: UIPanGestureRecognizer) {
switch panGesture.state { switch panGesture.state {
@@ -194,10 +230,15 @@ public class ImageZoomController: UIViewController {
extension ImageZoomController: UIScrollViewDelegate { extension ImageZoomController: UIScrollViewDelegate {
/// Tells the delegate that the scroll view has zoomed.
/// - Parameter scrollView: The scroll view that zoomed.
public func scrollViewDidZoom(_ scrollView: UIScrollView) { public func scrollViewDidZoom(_ scrollView: UIScrollView) {
delegate?.imageZoomController?(controller: self, didZoom: self.imageView.image) 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? { public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.imageView return self.imageView
} }

View File

@@ -20,14 +20,25 @@
// THE SOFTWARE. // THE SOFTWARE.
// MARK: - Framework headers // MARK: - Framework headers
#if canImport(UIKit)
import Foundation
import UIKit import UIKit
// MARK: - Protocols // MARK: - Protocols
/// A protocol that defines optional methods for `LCSnackBarView` delegate.
@objc @objc
public protocol LCSnackBarViewDelegate { public protocol LCSnackBarViewDelegate {
/// Called when the snackbar starts its exhibition.
/// - Parameter didStartExibition: The `LCSnackBarView` instance that started exhibition.
@objc optional func snackbar(didStartExibition: LCSnackBarView) @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) @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) @objc optional func snackbar(didEndExibition: LCSnackBarView)
} }
@@ -36,23 +47,36 @@ public protocol LCSnackBarViewDelegate {
// MARK: - Local Defines / ENUMS // MARK: - Local Defines / ENUMS
/// Enumeration defining the visual style of the `LCSnackBarView`.
public enum LCSnackBarViewType { 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 { 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 { public enum LCSnackBarTimer: CGFloat {
/// The snackbar remains visible indefinitely until dismissed manually.
case infinity = 0 case infinity = 0
/// Minimum display duration (2 seconds).
case minimum = 2 case minimum = 2
/// Medium display duration (5 seconds).
case medium = 5 case medium = 5
/// Maximum display duration (10 seconds).
case maximum = 10 case maximum = 10
} }
// MARK: - Class // 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: /// Usage example:
/// ///
@@ -62,7 +86,7 @@ public enum LCSnackBarTimer: CGFloat {
/// .configure(text: "Hello World!") /// .configure(text: "Hello World!")
/// .present() /// .present()
///``` ///```
///You can set delegate to interact with it ///You can set a delegate to interact with it:
/// ///
///```swift ///```swift
///let notification = LCSnackBarView(delegate: self) ///let notification = LCSnackBarView(delegate: self)
@@ -78,6 +102,7 @@ public final class LCSnackBarView: UIView {
// MARK: - Private properties // MARK: - Private properties
/// The content view that holds the snackbar's elements.
private lazy var contentView: UIView = { private lazy var contentView: UIView = {
$0.backgroundColor = .white $0.backgroundColor = .white
$0.translatesAutoresizingMaskIntoConstraints = false $0.translatesAutoresizingMaskIntoConstraints = false
@@ -85,6 +110,7 @@ public final class LCSnackBarView: UIView {
return $0 return $0
}(UIView()) }(UIView())
/// The label that displays the main text of the snackbar.
private lazy var descriptionLabel: UILabel = { private lazy var descriptionLabel: UILabel = {
$0.font = .systemFont(ofSize: 12, weight: .regular) $0.font = .systemFont(ofSize: 12, weight: .regular)
$0.text = nil $0.text = nil
@@ -117,10 +143,16 @@ public final class LCSnackBarView: UIView {
// MARK: - Public properties // MARK: - Public properties
/// The delegate for the snackbar view.
public weak var delegate: LCSnackBarViewDelegate? public weak var delegate: LCSnackBarViewDelegate?
// MARK: - Initializers // 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( public init(
style: LCSnackBarViewType = .default, style: LCSnackBarViewType = .default,
orientation: LCSnackBarOrientation = .top, orientation: LCSnackBarOrientation = .top,
@@ -161,12 +193,14 @@ public extension LCSnackBarView {
// MARK: - Private methods // MARK: - Private methods
/// Sets up the default layout properties for the snackbar.
private func setupDefaultLayout() { private func setupDefaultLayout() {
backgroundColor = .white backgroundColor = .white
contentView.backgroundColor = .white contentView.backgroundColor = .white
clipsToBounds = true clipsToBounds = true
} }
/// Sets up a tap gesture recognizer for the snackbar.
private func setupGestureRecognizer() { private func setupGestureRecognizer() {
let gesture = UITapGestureRecognizer(target: self, action: #selector(onTapGestureAction)) let gesture = UITapGestureRecognizer(target: self, action: #selector(onTapGestureAction))
gesture.numberOfTapsRequired = 1 gesture.numberOfTapsRequired = 1
@@ -174,6 +208,7 @@ public extension LCSnackBarView {
addGestureRecognizer(gesture) addGestureRecognizer(gesture)
} }
/// Configures observers for keyboard appearance and disappearance notifications.
private func setKeyboardObserver() { private func setKeyboardObserver() {
// Show // Show
NotificationCenter NotificationCenter
@@ -196,53 +231,41 @@ public extension LCSnackBarView {
) )
} }
@objc private func keyboardWillShow(_ notification: Notification?) -> Void { /// Handles the `keyboardWillShowNotification` to adjust the snackbar's position.
/// - Parameter notification: The `Notification` object containing keyboard information.
@MainActor
@objc private func keyboardWillShow(_ notification: Notification?) {
guard let info = notification?.userInfo else { return }
if let info = notification?.userInfo { systemKeyboardVisible = true
systemKeyboardVisible = true let animationCurveRaw = (info[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue
// ?? UInt(UIView.AnimationCurve.easeOut.rawValue)
let curveUserInfoKey = UIResponder.keyboardAnimationCurveUserInfoKey let animationDuration = (info[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue
let durationUserInfoKey = UIResponder.keyboardAnimationDurationUserInfoKey ?? 0.25
let frameEndUserInfoKey = UIResponder.keyboardFrameEndUserInfoKey let keyboardFrame = (info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
// ?? .zero
var animationCurve: UIView.AnimationOptions = .curveEaseOut let height = keyboardFrame.height
var animationDuration: TimeInterval = 0.25 let animationCurve = UIView.AnimationOptions(rawValue: animationCurveRaw << 16)
var height:CGFloat = 0.0
// Getting keyboard animation. UIView.animate(
if let curve = info[curveUserInfoKey] as? UIView.AnimationOptions { withDuration: animationDuration,
animationCurve = curve delay: 0,
} options: animationCurve
) { [weak self] in
// Getting keyboard animation duration self?.frame.origin.y += height
if let duration = info[durationUserInfoKey] as? TimeInterval {
animationDuration = duration
}
// Getting UIKeyboardSize.
if let kbFrame = info[frameEndUserInfoKey] as? CGRect {
height = kbFrame.size.height
}
DispatchQueue.main.async { [weak self] in
UIView.animate(withDuration: animationDuration,
delay: 0,
options: animationCurve,
animations: {
self?.frame.origin.y += height
})
}
} }
} }
@objc private func keyboardWillHide(_ notification: Notification?) -> Void { /// Handles the `keyboardWillHideNotification`.
DispatchQueue.main.async { [weak self] in /// - Parameter notification: The `Notification` object.
self?.systemKeyboardVisible = false @MainActor
// keyboard is hidded @objc private func keyboardWillHide(_ notification: Notification?) {
} systemKeyboardVisible = false
// keyboard is hidded
} }
/// Updates the snackbar's style properties, such as width and corner radius, based on `_style`.
private func updateStyle() { private func updateStyle() {
switch _style { switch _style {
case .rounded: case .rounded:
@@ -256,6 +279,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) { private func positioningView(_ view: UIView) {
view view
.addSubview(self, .addSubview(self,
@@ -286,6 +311,10 @@ public extension LCSnackBarView {
height: _height) 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)) { private func showSnackBar(controller: UIViewController, completion: @escaping (() -> Void)) {
if isOpen { if isOpen {
closeSnackBar(controller: controller) { closeSnackBar(controller: controller) {
@@ -320,6 +349,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)) { private func closeSnackBar(controller: UIViewController, completion: @escaping (() -> Void)) {
let distance = CGFloat(_style == .rounded ? (_orientation == .top ? UIDevice.topNotch : UIDevice.bottomNotch) : 0) let distance = CGFloat(_style == .rounded ? (_orientation == .top ? UIDevice.topNotch : UIDevice.bottomNotch) : 0)
layoutIfNeeded() layoutIfNeeded()
@@ -341,6 +374,8 @@ public extension LCSnackBarView {
} }
} }
/// Handles tap gestures on the snackbar.
/// - Parameter _: The `UITapGestureRecognizer` instance.
@objc @objc
private func onTapGestureAction(_ : UITapGestureRecognizer) { private func onTapGestureAction(_ : UITapGestureRecognizer) {
self.delegate?.snackbar?(didTouchOn: self) self.delegate?.snackbar?(didTouchOn: self)
@@ -350,6 +385,7 @@ public extension LCSnackBarView {
} }
} }
/// Adds the subviews to the snackbar and sets up their constraints.
private func addComponentsAndConstraints() { private func addComponentsAndConstraints() {
// MARK: - Add Subviews // MARK: - Add Subviews
@@ -376,18 +412,29 @@ public extension LCSnackBarView {
// MARK: - Public methods // MARK: - Public methods
/// Configures the text displayed in the snackbar.
/// - Parameter text: The `String` text to display.
/// - Returns: The `LCSnackBarView` instance for chaining.
@discardableResult @discardableResult
func configure(text: String) -> Self { func configure(text: String) -> Self {
descriptionLabel.text = text descriptionLabel.text = text
return self 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 @discardableResult
func configure(textColor: UIColor) -> Self { func configure(textColor: UIColor) -> Self {
descriptionLabel.textColor = textColor descriptionLabel.textColor = textColor
return self 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 @discardableResult
func configure(textFont: UIFont, alignment: NSTextAlignment = .center) -> Self { func configure(textFont: UIFont, alignment: NSTextAlignment = .center) -> Self {
descriptionLabel.font = textFont descriptionLabel.font = textFont
@@ -395,6 +442,9 @@ public extension LCSnackBarView {
return self return self
} }
/// Configures the background color of the snackbar.
/// - Parameter backgroundColor: The `UIColor` for the background.
/// - Returns: The `LCSnackBarView` instance for chaining.
@discardableResult @discardableResult
func configure(backgroundColor: UIColor) -> Self { func configure(backgroundColor: UIColor) -> Self {
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
@@ -402,12 +452,20 @@ public extension LCSnackBarView {
return self return self
} }
/// Configures the exhibition duration (timer) of the snackbar.
/// - Parameter timer: The `LCSnackBarTimer` value.
/// - Returns: The `LCSnackBarView` instance for chaining.
@discardableResult @discardableResult
func configure(exibition timer: LCSnackBarTimer) -> Self { func configure(exibition timer: LCSnackBarTimer) -> Self {
_timer = timer _timer = timer
return self 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 @discardableResult
func configure(imageIconBefore icon: UIImageView, withTintColor: UIColor? = nil) -> Self { func configure(imageIconBefore icon: UIImageView, withTintColor: UIColor? = nil) -> Self {
icon.setHeight(size: 24) icon.setHeight(size: 24)
@@ -428,6 +486,8 @@ public extension LCSnackBarView {
return self 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) { func present(completion: (()->())? = nil) {
if isOpen { return } if isOpen { return }
if let controller = LCEssentials.getTopViewController(aboveBars: true) { if let controller = LCEssentials.getTopViewController(aboveBars: true) {
@@ -437,3 +497,5 @@ public extension LCSnackBarView {
} }
} }
} }
#endif

View File

@@ -18,31 +18,54 @@
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // 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 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE. // THE SOFTWARE.
#if canImport(SwiftUI) #if canImport(SwiftUI) && os(iOS)
import 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, *) @available(iOS 15, *)
class LCENavigationState: ObservableObject { class LCENavigationState: ObservableObject {
/// The image view for the right button.
@Published var rightButtonImage: AnyView? = nil @Published var rightButtonImage: AnyView? = nil
/// The text for the right button.
@Published var rightButtonText: Text = Text("") @Published var rightButtonText: Text = Text("")
/// The action to perform when the right button is tapped.
@Published var rightButtonAction: () -> Void = {} @Published var rightButtonAction: () -> Void = {}
/// The image view for the left button.
@Published var leftButtonImage: AnyView? = nil @Published var leftButtonImage: AnyView? = nil
/// The text for the left button.
@Published var leftButtonText: Text = Text("") @Published var leftButtonText: Text = Text("")
/// The action to perform when the left button is tapped.
@Published var leftButtonAction: () -> Void = {} @Published var leftButtonAction: () -> Void = {}
/// A boolean value that controls the visibility of the navigation bar.
@Published var hideNavigationBar: Bool = false @Published var hideNavigationBar: Bool = false
/// The title view of the navigation bar.
@Published var title: (any View) = Text("") @Published var title: (any View) = Text("")
/// The subtitle view of the navigation bar.
@Published var subTitle: (any View) = Text("") @Published var subTitle: (any View) = Text("")
/// The background color of the navigation bar.
@Published var navigationBarBackgroundColor: Color = .clear
} }
/// `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, *) @available(iOS 15, *)
public struct LCENavigationView<Content: View>: View { public struct LCENavigationView<Content: View>: View {
/// The observed state object for the navigation view.
@ObservedObject private var state: LCENavigationState @ObservedObject private var state: LCENavigationState
/// The content view displayed below the navigation bar.
let content: Content 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( public init(
title: (any View) = Text(""), title: (any View) = Text(""),
subTitle: (any View) = Text(""), subTitle: (any View) = Text(""),
@@ -56,8 +79,9 @@ public struct LCENavigationView<Content: View>: View {
self.state.subTitle = subTitle self.state.subTitle = subTitle
} }
/// The body of the `LCENavigationView`.
public var body: some View { public var body: some View {
VStack { VStack(spacing: 0) {
if !state.hideNavigationBar { if !state.hideNavigationBar {
NavigationBarView NavigationBarView
} }
@@ -66,6 +90,7 @@ public struct LCENavigationView<Content: View>: View {
.navigationBarHidden(true) .navigationBarHidden(true)
} }
/// The private `NavigationBarView` that lays out the navigation bar components.
private var NavigationBarView: some View { private var NavigationBarView: some View {
HStack { HStack {
NavLeftButton NavLeftButton
@@ -77,10 +102,11 @@ public struct LCENavigationView<Content: View>: View {
.font(.headline) .font(.headline)
.padding() .padding()
.background { .background {
Color.clear.ignoresSafeArea(edges: .top) state.navigationBarBackgroundColor.ignoresSafeArea(edges: .top)
} }
} }
/// The private `TitleView` that displays the title and subtitle.
private var TitleView: some View { private var TitleView: some View {
VStack { VStack {
AnyView(state.title) AnyView(state.title)
@@ -90,6 +116,7 @@ public struct LCENavigationView<Content: View>: View {
} }
} }
/// The private `NavLeftButton` view.
private var NavLeftButton: some View { private var NavLeftButton: some View {
Button(action: state.leftButtonAction) { Button(action: state.leftButtonAction) {
HStack { HStack {
@@ -101,6 +128,7 @@ public struct LCENavigationView<Content: View>: View {
} }
} }
/// The private `NavRightButton` view.
private var NavRightButton: some View { private var NavRightButton: some View {
Button(action: state.rightButtonAction) { Button(action: state.rightButtonAction) {
HStack { HStack {
@@ -112,6 +140,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( public func setRightButton(
text: Text = Text(""), text: Text = Text(""),
image: (any View)? = nil, image: (any View)? = nil,
@@ -135,6 +169,12 @@ public struct LCENavigationView<Content: View>: View {
return self 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( public func setLeftButton(
text: Text = Text(""), text: Text = Text(""),
image: (any View)? = nil, image: (any View)? = nil,
@@ -158,6 +198,11 @@ public struct LCENavigationView<Content: View>: View {
return self 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( public func setTitle(
text: (any View) = Text(""), text: (any View) = Text(""),
subTitle: (any View)? = nil subTitle: (any View)? = nil
@@ -167,14 +212,29 @@ public struct LCENavigationView<Content: View>: View {
return self 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 { public func hideNavigationView(_ hide: Bool) -> LCENavigationView {
state.hideNavigationBar = hide state.hideNavigationBar = hide
return self return self
} }
/// Sets the background color for the navigation bar.
/// - Parameter color: The color to use as the navigation bar background.
/// - Returns: The `LCENavigationView` instance for chaining.
public func setNavigationBarBackgroundColor(_ color: Color) -> LCENavigationView {
state.navigationBarBackgroundColor = color
return self
}
} }
/// Extension to `FormatStyle` to format any value as a string.
@available(iOS 15.0, *) @available(iOS 15.0, *)
extension FormatStyle { 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? { func format(any value: Any) -> FormatOutput? {
if let v = value as? FormatInput { if let v = value as? FormatInput {
return format(v) return format(v)
@@ -183,8 +243,11 @@ extension FormatStyle {
} }
} }
/// Extension to `LocalizedStringKey` to resolve localized strings.
@available(iOS 15.0, *) @available(iOS 15.0, *)
extension LocalizedStringKey { extension LocalizedStringKey {
/// Resolves the localized string key into a `String`.
/// - Returns: The resolved string, or `nil` if resolution fails.
var resolved: String? { var resolved: String? {
let mirror = Mirror(reflecting: self) let mirror = Mirror(reflecting: self)
guard let key = mirror.descendant("key") as? String else { guard let key = mirror.descendant("key") as? String else {
@@ -237,8 +300,11 @@ extension LocalizedStringKey {
} }
} }
/// Extension to `Text` to retrieve its string content.
@available(iOS 15.0, *) @available(iOS 15.0, *)
extension Text { extension Text {
/// Returns the string representation of the `Text` view.
/// - Returns: The string content, or `nil` if it cannot be extracted.
var string: String? { var string: String? {
let mirror = Mirror(reflecting: self) let mirror = Mirror(reflecting: self)
if let s = mirror.descendant("storage", "verbatim") as? String { if let s = mirror.descendant("storage", "verbatim") as? String {