15 Commits

Author SHA1 Message Date
Daniel Arantes Loverde
4f84dfb108 Update README.md 2025-09-02 17:28:28 -03: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
16 changed files with 407 additions and 463 deletions

15
Package.resolved Normal file
View File

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

View File

@@ -1,16 +1,33 @@
// swift-tools-version: 6.0 // swift-tools-version: 6.0
import PackageDescription import PackageDescription
import Foundation
let isLocalDevelopment = FileManager.default.fileExists(atPath: "../LCECryptoKit/PrivateLib/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: [
.package(
url: isLocalDevelopment ?
"../LCECryptoKit/PrivateLib/LCECryptoKitBinary" :
"https://60c260c85d3a2fe840411b0ff98f521b5eca3c56@git.loverde.com.br/Loverde-Company-LTDA/LCECryptoKitBinary.git",
exact: "1.0.0"
)
],
targets: [ targets: [
.target( .target(
name: "LCEssentials"), name: "LCEssentials",
dependencies: [.product(name: "LCECryptoKit", package: "lcecryptokitbinary")]),
] ]
) )

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

View File

@@ -0,0 +1,41 @@
//
// Copyright (c) 2025 Loverde Co.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
import LCECryptoKit
public final class LCECrypto {
private let hashKey: String
init(privateKey: String){
self.hashKey = privateKey
}
func encodeOTP(email: String, password: String) -> String? {
return LCECryptoKit.encodeSeed(email: email, password: password, hashKey: self.hashKey)
}
func decodeOTP(_ otpHash: String) -> Bool {
LCECryptoKit.decodeSeed(otpKey: otpHash, hashKey: self.hashKey)
}
}

View File

@@ -29,18 +29,30 @@ import Security
import UIKit import UIKit
#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 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 +60,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`).
/// - 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,
@@ -83,7 +118,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 +126,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 +177,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 +233,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 +268,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 +280,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 +289,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 +332,16 @@ private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
} }
} }
/// Retrieves the `SecIdentity` from the provided certificate data and password.
///
/// - Returns: A `SecIdentity` object if the certificate is successfully imported, otherwise `nil`.
private func getIdentity() -> SecIdentity? { 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 +356,12 @@ private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
} }
/// Checks if the provided data is a valid PKCS#12 (P12) certificate with the given password.
///
/// - Parameters:
/// - data: The `Data` representation of the certificate.
/// - password: The password for the certificate.
/// - Returns: `true` if the data is a valid P12 certificate with the given password, otherwise `false`.
private func isPKCS12(data: Data, password: String) -> Bool { 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 +374,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 +385,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 +408,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 +475,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 +507,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,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

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

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

@@ -1,4 +1,4 @@
// //
// Copyright (c) 2023 Loverde Co. // Copyright (c) 2023 Loverde Co.
// //
// Permission is hereby granted, free of charge, to any person obtaining a copy // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -25,17 +25,29 @@ import UIKit
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 +64,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 +100,10 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
} }
/// Presents an `UIAlertController` with options to open the camera, photo roll, or grant permissions.
/// - Parameters:
/// - forCamera: A boolean indicating whether camera access is granted. Defaults to `true`.
/// - forAlbum: A boolean indicating whether photo album access is granted. Defaults to `true`.
private func openAlerts(forCamera:Bool = true, forAlbum:Bool = true){ 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 +136,9 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
} }
} }
/// Opens the camera device using `UIImagePickerController`.
///
/// If the camera is not available, an alert message is presented to the user.
private func openCameraDevice(){ 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 +152,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 +184,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 +214,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 +223,10 @@ extension ImagePickerController: UIImagePickerControllerDelegate {
}) })
} }
/// Tells the delegate that the user picked an image or movie.
/// - Parameters:
/// - picker: The image picker controller.
/// - info: A dictionary containing the original image, and possibly an edited image or a movie URL.
@objc public func imagePickerController(_ picker: UIImagePickerController, @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

@@ -1,4 +1,4 @@
// //
// Copyright (c) 2023 Loverde Co. // Copyright (c) 2023 Loverde Co.
// //
// Permission is hereby granted, free of charge, to any person obtaining a copy // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -22,13 +22,25 @@
import UIKit import UIKit
#if os(iOS) || os(macOS) #if os(iOS) || os(macOS)
/// 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 +49,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 +57,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 +69,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 +77,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 +124,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 +148,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 +161,29 @@ public class ImageZoomController: UIViewController {
} }
} }
/// Dismisses the `ImageZoomController`.
/// - Parameter completion: An optional closure to be executed after the dismissal is complete.
public func dismiss(completion: (()->())? = nil) { 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 +228,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

@@ -24,10 +24,19 @@ 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 +45,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 +84,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 +100,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 +108,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 +141,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 +191,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 +206,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,6 +229,8 @@ public extension LCSnackBarView {
) )
} }
/// Handles the `keyboardWillShowNotification` to adjust the snackbar's position.
/// - Parameter notification: The `Notification` object containing keyboard information.
@objc private func keyboardWillShow(_ notification: Notification?) -> Void { @objc private func keyboardWillShow(_ notification: Notification?) -> Void {
if let info = notification?.userInfo { if let info = notification?.userInfo {
@@ -236,6 +271,8 @@ public extension LCSnackBarView {
} }
} }
/// Handles the `keyboardWillHideNotification`.
/// - Parameter notification: The `Notification` object.
@objc private func keyboardWillHide(_ notification: Notification?) -> Void { @objc private func keyboardWillHide(_ notification: Notification?) -> Void {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.systemKeyboardVisible = false self?.systemKeyboardVisible = false
@@ -243,6 +280,7 @@ public extension LCSnackBarView {
} }
} }
/// Updates the snackbar's style properties, such as width and corner radius, based on `_style`.
private func updateStyle() { private func updateStyle() {
switch _style { switch _style {
case .rounded: case .rounded:
@@ -256,6 +294,8 @@ public extension LCSnackBarView {
} }
} }
/// Positions the snackbar view within the given superview based on its `_orientation`.
/// - Parameter view: The `UIView` that will contain the snackbar.
private func positioningView(_ view: UIView) { private func positioningView(_ view: UIView) {
view view
.addSubview(self, .addSubview(self,
@@ -286,6 +326,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 +364,10 @@ public extension LCSnackBarView {
} }
} }
/// Hides the snackbar with an animation.
/// - Parameters:
/// - controller: The `UIViewController` from which the snackbar is being dismissed.
/// - completion: A closure to be executed once the dismissal animation completes.
private func closeSnackBar(controller: UIViewController, completion: @escaping (() -> Void)) { 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 +389,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 +400,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 +427,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 +457,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 +467,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 +501,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) {

View File

@@ -1,4 +1,4 @@
// //
// Copyright (c) 2024 Loverde Co. // Copyright (c) 2024 Loverde Co.
// //
// Permission is hereby granted, free of charge, to any person obtaining a copy // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -21,28 +21,49 @@
#if canImport(SwiftUI) #if canImport(SwiftUI)
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("")
} }
/// `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,6 +77,7 @@ 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 {
if !state.hideNavigationBar { if !state.hideNavigationBar {
@@ -66,6 +88,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
@@ -81,6 +104,7 @@ public struct LCENavigationView<Content: View>: View {
} }
} }
/// 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 +114,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 +126,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 +138,12 @@ public struct LCENavigationView<Content: View>: View {
} }
} }
/// Sets the configuration for the right button of the navigation bar.
/// - Parameters:
/// - text: The `Text` to display on the button. Defaults to an empty `Text`.
/// - image: An optional `View` to use as the button's icon. Defaults to `nil`.
/// - action: The closure to execute when the button is tapped.
/// - Returns: The `LCENavigationView` instance for chaining.
public func setRightButton( public func setRightButton(
text: Text = Text(""), text: Text = Text(""),
image: (any View)? = nil, image: (any View)? = nil,
@@ -135,6 +167,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 +196,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 +210,21 @@ 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
} }
} }
/// 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 +233,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 +290,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 {