Compare commits
15 Commits
8f8cffc8ba
...
feature/LC
Author | SHA1 | Date | |
---|---|---|---|
729812d20b | |||
08519d1aca | |||
![]() |
7ac2ccb21f | ||
![]() |
d2ca6e54d2 | ||
![]() |
0eb4f355df | ||
![]() |
3e3e181b36 | ||
![]() |
175816dff8 | ||
![]() |
daae48817a | ||
![]() |
241d69ecc1 | ||
![]() |
f4fade0442 | ||
![]() |
0910973e9a | ||
![]() |
189efd7154 | ||
![]() |
578c69e0f8 | ||
![]() |
f0f9f1bace | ||
![]() |
6040caa2de |
15
Package.resolved
Normal file
15
Package.resolved
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"originHash" : "c64eb123858a637db7dce26475a35db7adb60ead911617c384beba36afa95bc5",
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "lcecryptokitbinary",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://60c260c85d3a2fe840411b0ff98f521b5eca3c56@git.loverde.com.br/Loverde-Company-LTDA/LCECryptoKitBinary.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2e7850fdb14cacf6bf2eb160f64c3df84cf5b1c4",
|
||||||
|
"version" : "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 3
|
||||||
|
}
|
@@ -1,16 +1,33 @@
|
|||||||
// swift-tools-version: 6.0
|
// 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")]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
41
Sources/LCEssentials/Classes/LCECrypto.swift
Normal file
41
Sources/LCEssentials/Classes/LCECrypto.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) 2025 Loverde Co.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import LCECryptoKit
|
||||||
|
|
||||||
|
public final class LCECrypto {
|
||||||
|
|
||||||
|
private let hashKey: String
|
||||||
|
|
||||||
|
init(privateKey: String){
|
||||||
|
self.hashKey = privateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeOTP(email: String, password: String) -> String? {
|
||||||
|
return LCECryptoKit.encodeSeed(email: email, password: password, hashKey: self.hashKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeOTP(_ otpHash: String) -> Bool {
|
||||||
|
LCECryptoKit.decodeSeed(otpKey: otpHash, hashKey: self.hashKey)
|
||||||
|
}
|
||||||
|
}
|
@@ -29,18 +29,30 @@ import Security
|
|||||||
import UIKit
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -1,271 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright (c) 2020 Loverde Co.
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
#if os(iOS) || os(macOS)
|
|
||||||
@objc public protocol HUDAlertViewControllerDelegate {
|
|
||||||
@objc optional func didOpen(alert: HUDAlertViewController)
|
|
||||||
@objc optional func didClose(alert: HUDAlertViewController)
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum HUDAlertActionType: Int {
|
|
||||||
case cancel = 0
|
|
||||||
case normal = 1
|
|
||||||
case destructive = 2
|
|
||||||
case discrete = 3
|
|
||||||
case green = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HUDAlertAction {
|
|
||||||
|
|
||||||
public typealias CompletionBlock = () -> Void
|
|
||||||
|
|
||||||
var title: String = ""
|
|
||||||
var tag: Int = 0
|
|
||||||
var type: HUDAlertActionType = .cancel
|
|
||||||
var completionBlock: CompletionBlock? = nil
|
|
||||||
|
|
||||||
public convenience init(title: String, type: HUDAlertActionType, _ completion: (() -> Void)? = nil) {
|
|
||||||
self.init()
|
|
||||||
//
|
|
||||||
self.title = title
|
|
||||||
self.type = type
|
|
||||||
self.completionBlock = completion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HUDAlertViewController: UIViewController {
|
|
||||||
|
|
||||||
fileprivate var viewController: UIViewController
|
|
||||||
|
|
||||||
lazy var actions: [HUDAlertAction] = []
|
|
||||||
|
|
||||||
let greenColor: UIColor = UIColor(hex: "609c70")
|
|
||||||
let redColor: UIColor = UIColor(hex: "a32a2e")
|
|
||||||
let blueColor: UIColor = UIColor(hex: "2a50a8")
|
|
||||||
let greyColor: UIColor = UIColor(hex: "a6a6a6")
|
|
||||||
|
|
||||||
public weak var delegate: HUDAlertViewControllerDelegate?
|
|
||||||
public var isLoadingAlert: Bool = true
|
|
||||||
|
|
||||||
public var customView: HUDAlertView {
|
|
||||||
return (view as? HUDAlertView) ?? HUDAlertView()
|
|
||||||
}
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
self.viewController = UIViewController()
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(*, unavailable)
|
|
||||||
required public init?(coder _: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
public override func loadView() {
|
|
||||||
view = HUDAlertView()
|
|
||||||
}
|
|
||||||
|
|
||||||
public override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
if isLoaded {
|
|
||||||
printLog(title: "VIEW DIDLOAD", msg: "IS LOADED", prettyPrint: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setTitle(_ title: String = "", font: UIFont = UIFont.systemFont(ofSize: 16.0), color: UIColor = .black) {
|
|
||||||
self.customView.titleLabel.text = title
|
|
||||||
self.customView.titleLabel.font = font
|
|
||||||
self.customView.titleLabel.textColor = color
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setDescription(_ desc: String = "", font: UIFont = .systemFont(ofSize: 14.0), color: UIColor = .black) {
|
|
||||||
self.customView.descLabel.text = desc
|
|
||||||
self.customView.descLabel.font = font
|
|
||||||
self.customView.descLabel.textColor = color
|
|
||||||
}
|
|
||||||
|
|
||||||
public func configureAlertWith(title: String,
|
|
||||||
description: String? = nil,
|
|
||||||
viewController: UIViewController? = nil,
|
|
||||||
actionButtons: [HUDAlertAction] = []) {
|
|
||||||
|
|
||||||
if let viewController {
|
|
||||||
self.viewController = viewController
|
|
||||||
} else if let topViewController = LCEssentials.getTopViewController(aboveBars: true) {
|
|
||||||
self.viewController = topViewController
|
|
||||||
} else {
|
|
||||||
fatalError("Ops! No UIViewController was found.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.isLoadingAlert {
|
|
||||||
self.customView.rotatinProgress.progress = 0.8
|
|
||||||
self.customView.rotatinProgress.heightConstraint?.constant = 40
|
|
||||||
}else{
|
|
||||||
self.customView.rotatinProgress.progress = 0
|
|
||||||
self.customView.rotatinProgress.heightConstraint?.constant = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
self.customView.containerView.cornerRadius = 8
|
|
||||||
self.customView.titleLabel.text = title
|
|
||||||
self.customView.descLabel.text = description
|
|
||||||
self.actions.removeAll()
|
|
||||||
self.customView.stackButtons.removeAllArrangedSubviews()
|
|
||||||
|
|
||||||
if !actionButtons.isEmpty {
|
|
||||||
|
|
||||||
for (index, element) in actionButtons.enumerated() {
|
|
||||||
|
|
||||||
lazy var button: UIButton = {
|
|
||||||
$0.isOpaque = true
|
|
||||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
$0.setTitleForAllStates(element.title)
|
|
||||||
$0.tag = index
|
|
||||||
$0.isExclusiveTouch = true
|
|
||||||
$0.isUserInteractionEnabled = true
|
|
||||||
$0.cornerRadius = 5
|
|
||||||
$0.setHeight(size: 47.0)
|
|
||||||
$0.addTarget(self, action: #selector(self.actionButton(sender:)), for: .touchUpInside)
|
|
||||||
return $0
|
|
||||||
}(UIButton(type: .custom))
|
|
||||||
|
|
||||||
switch element.type {
|
|
||||||
case .cancel:
|
|
||||||
button.borderWidth = 1
|
|
||||||
button.borderColor = self.blueColor
|
|
||||||
button.backgroundColor = .white
|
|
||||||
button.setTitleColorForAllStates(self.blueColor)
|
|
||||||
case .destructive:
|
|
||||||
button.borderWidth = 0
|
|
||||||
button.borderColor = nil
|
|
||||||
button.backgroundColor = self.redColor
|
|
||||||
button.setTitleColorForAllStates(.white)
|
|
||||||
case .normal:
|
|
||||||
button.borderWidth = 0
|
|
||||||
button.borderColor = nil
|
|
||||||
button.backgroundColor = self.blueColor
|
|
||||||
button.setTitleColorForAllStates(.white)
|
|
||||||
case .discrete:
|
|
||||||
button.borderWidth = 0
|
|
||||||
button.borderColor = nil
|
|
||||||
button.backgroundColor = self.greyColor
|
|
||||||
button.setTitleColorForAllStates(.white)
|
|
||||||
case .green:
|
|
||||||
button.borderWidth = 0
|
|
||||||
button.borderColor = nil
|
|
||||||
button.backgroundColor = self.greenColor
|
|
||||||
button.setTitleColorForAllStates(.white)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.customView.stackButtons.isHidden = false
|
|
||||||
self.customView.stackButtons.spacing = 10
|
|
||||||
self.customView.stackButtons.heightConstraint?.isActive = false
|
|
||||||
self.customView.stackButtons.addArrangedSubview(button)
|
|
||||||
self.actions.append(element)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.actions.removeAll()
|
|
||||||
self.customView.stackButtons.removeAllArrangedSubviews()
|
|
||||||
self.customView.stackButtons.spacing = 0
|
|
||||||
self.customView.stackButtons.heightConstraint?.isActive = true
|
|
||||||
self.customView.stackButtons.heightConstraint?.constant = 0
|
|
||||||
self.customView.stackButtons.isHidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func actionButton(sender: UIButton) {
|
|
||||||
self.actions[exist: sender.tag]?.completionBlock?()
|
|
||||||
self.closeAlert()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func showAlert(){
|
|
||||||
if self.presentingViewController != nil {
|
|
||||||
self.closeAlert()
|
|
||||||
LCEssentials.backgroundThread(delay: 0.2, completion: {
|
|
||||||
self.showAlert()
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.modalTransitionStyle = .crossDissolve
|
|
||||||
self.modalPresentationStyle = .overFullScreen
|
|
||||||
|
|
||||||
self.viewController.present(self, animated: false, completion: {
|
|
||||||
self.animateIn {
|
|
||||||
self.delegate?.didOpen?(alert: self)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public func closeAlert(){
|
|
||||||
animateOut {
|
|
||||||
self.dismiss(animated: false) {
|
|
||||||
self.delegate?.didClose?(alert: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func animateIn(completion: @escaping() -> Void) {
|
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.1, delay: 0.0, options: [.allowUserInteraction, .allowAnimatedContent, .curveEaseOut], animations: {
|
|
||||||
self.view.alpha = 1.0
|
|
||||||
}) { (completed) in
|
|
||||||
self.performSpringAnimationIn(for: self.customView.containerView) {
|
|
||||||
completion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func animateOut(completion: @escaping() -> Void) {
|
|
||||||
|
|
||||||
self.performSpringAnimationOut(for: self.customView.containerView) {
|
|
||||||
UIView.animate(withDuration: 0.1, delay: 0.0, options: [.allowUserInteraction, .allowAnimatedContent, .curveEaseIn], animations: {
|
|
||||||
self.view.alpha = 0.0
|
|
||||||
}) { (completed) in
|
|
||||||
completion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func performSpringAnimationIn(for view: UIView, completion: @escaping() -> Void) {
|
|
||||||
view.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
|
||||||
//
|
|
||||||
UIView.animate(withDuration: 0.15, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseIn, animations: {
|
|
||||||
view.transform = CGAffineTransform(scaleX: 1, y: 1)
|
|
||||||
view.alpha = 1.0
|
|
||||||
}) { (completed) in
|
|
||||||
completion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private func performSpringAnimationOut(for view: UIView, completion: @escaping() -> Void) {
|
|
||||||
UIView.animate(withDuration: 0.15, animations: {
|
|
||||||
view.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
|
||||||
view.alpha = 0.0
|
|
||||||
}) { (completed) in
|
|
||||||
completion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
@@ -1,158 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright (c) 2022 Loverde Co.
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Framework headers
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
// MARK: - Protocols
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Interface Headers
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Local Defines / ENUMS
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Class
|
|
||||||
|
|
||||||
public final class HUDAlertView: UIView {
|
|
||||||
|
|
||||||
// MARK: - Private properties
|
|
||||||
|
|
||||||
// MARK: - Internal properties
|
|
||||||
|
|
||||||
lazy var containerView: UIView = {
|
|
||||||
$0.isOpaque = true
|
|
||||||
$0.backgroundColor = .white
|
|
||||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return $0
|
|
||||||
}(UIView())
|
|
||||||
|
|
||||||
let rotatinProgress: RotatingCircularGradientProgressBar = {
|
|
||||||
$0.layer.masksToBounds = true
|
|
||||||
$0.color = .gray
|
|
||||||
$0.gradientColor = .gray
|
|
||||||
$0.ringWidth = 2
|
|
||||||
$0.progress = 0
|
|
||||||
return $0
|
|
||||||
}(RotatingCircularGradientProgressBar())
|
|
||||||
|
|
||||||
lazy var titleLabel: UILabel = {
|
|
||||||
$0.font = UIFont.systemFont(ofSize: 22.0)
|
|
||||||
$0.textColor = .black
|
|
||||||
$0.backgroundColor = UIColor.clear
|
|
||||||
$0.numberOfLines = 0
|
|
||||||
$0.textAlignment = .center
|
|
||||||
$0.isOpaque = true
|
|
||||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return $0
|
|
||||||
}(UILabel())
|
|
||||||
|
|
||||||
lazy var descLabel: UILabel = {
|
|
||||||
$0.font = UIFont.systemFont(ofSize: 16.0)
|
|
||||||
$0.textColor = .black
|
|
||||||
$0.backgroundColor = UIColor.clear
|
|
||||||
$0.numberOfLines = 0
|
|
||||||
$0.textAlignment = .center
|
|
||||||
$0.isOpaque = true
|
|
||||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return $0
|
|
||||||
}(UILabel())
|
|
||||||
|
|
||||||
lazy var stackButtons: UIStackView = {
|
|
||||||
$0.axis = .vertical
|
|
||||||
$0.spacing = 10.0
|
|
||||||
$0.distribution = .fill
|
|
||||||
$0.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
|
||||||
$0.isLayoutMarginsRelativeArrangement = true
|
|
||||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return $0
|
|
||||||
}(UIStackView())
|
|
||||||
|
|
||||||
// MARK: - Public properties
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Initializers
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
super.init(frame: .zero)
|
|
||||||
|
|
||||||
insertBlurView(style: .dark, color: .clear, alpha: 0.8)
|
|
||||||
|
|
||||||
addComponentsAndConstraints()
|
|
||||||
}
|
|
||||||
|
|
||||||
required public init?(coder aDecoder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Super Class Overrides
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Private methods
|
|
||||||
|
|
||||||
fileprivate func addComponentsAndConstraints() {
|
|
||||||
|
|
||||||
// MARK: - Add Subviews
|
|
||||||
|
|
||||||
containerView.addSubviews([rotatinProgress, titleLabel, descLabel, stackButtons])
|
|
||||||
|
|
||||||
addSubviews([containerView])
|
|
||||||
|
|
||||||
// MARK: - Add Constraints
|
|
||||||
|
|
||||||
rotatinProgress
|
|
||||||
.setConstraintsTo(containerView, .top, 10)
|
|
||||||
.setConstraints(.centerX, 0)
|
|
||||||
rotatinProgress.setWidth(size: 40)
|
|
||||||
rotatinProgress.setHeight(size: 40)
|
|
||||||
|
|
||||||
titleLabel
|
|
||||||
.setConstraintsTo(rotatinProgress, .topToBottom, 8)
|
|
||||||
.setConstraintsTo(containerView, .leading, 10)
|
|
||||||
.setConstraints(.trailing, -10)
|
|
||||||
|
|
||||||
descLabel
|
|
||||||
.setConstraintsTo(titleLabel, .topToBottom, 8)
|
|
||||||
.setConstraintsTo(containerView, .leading, 10)
|
|
||||||
.setConstraints(.trailing, -10)
|
|
||||||
|
|
||||||
stackButtons
|
|
||||||
.setConstraintsTo(descLabel, .topToBottom, 8)
|
|
||||||
.setConstraintsTo(containerView, .leading, 10)
|
|
||||||
.setConstraints(.leading, 10)
|
|
||||||
.setConstraints(.trailing, -10)
|
|
||||||
.setConstraints(.bottom, -8)
|
|
||||||
|
|
||||||
containerView
|
|
||||||
.setConstraintsTo(self, .topToTopGreaterThanOrEqualTo, 20, true)
|
|
||||||
.setConstraints(.leading, 40)
|
|
||||||
.setConstraints(.trailing, -40)
|
|
||||||
.setConstraints(.centerX, 0)
|
|
||||||
.setConstraints(.centerY, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Internal methods
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Extensions
|
|
Binary file not shown.
Before Width: | Height: | Size: 68 KiB |
Binary file not shown.
Before Width: | Height: | Size: 75 KiB |
@@ -1,4 +1,4 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) 2023 Loverde Co.
|
// 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 {
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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 {
|
||||||
|
Reference in New Issue
Block a user