documentation
This commit is contained in:
@@ -29,18 +29,30 @@ import Security
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
/// A generic `Result` enumeration to represent either a success `Value` or a failure `Error`.
|
||||
public enum Result<Value, Error: Swift.Error> {
|
||||
/// Indicates a successful operation with an associated `Value`.
|
||||
case success(Value)
|
||||
/// Indicates a failed operation with an associated `Error`.
|
||||
case failure(Error)
|
||||
}
|
||||
|
||||
/// Enumeration defining common HTTP methods.
|
||||
public enum httpMethod: String {
|
||||
/// The POST method.
|
||||
case post = "POST"
|
||||
/// The GET method.
|
||||
case get = "GET"
|
||||
/// The PUT method.
|
||||
case put = "PUT"
|
||||
/// The DELETE method.
|
||||
case delete = "DELETE"
|
||||
}
|
||||
/// Loverde Co.: API generic struct for simple requests
|
||||
|
||||
/// Loverde Co.: API generic struct for simple requests.
|
||||
///
|
||||
/// This struct provides a convenient way to perform network requests with various configurations,
|
||||
/// including handling different HTTP methods, parameter encoding, and certificate-based authentication.
|
||||
@available(iOS 13.0.0, *)
|
||||
@MainActor
|
||||
public struct API {
|
||||
@@ -48,20 +60,43 @@ public struct API {
|
||||
private static var certData: Data?
|
||||
private static var certPassword: String?
|
||||
|
||||
/// The default error used when an unexpected issue occurs during a request.
|
||||
static let defaultError = NSError.createErrorWith(code: LCEssentials.DEFAULT_ERROR_CODE,
|
||||
description: LCEssentials.DEFAULT_ERROR_MSG,
|
||||
reasonForError: LCEssentials.DEFAULT_ERROR_MSG)
|
||||
|
||||
/// The delay in seconds before retrying a persistent connection request.
|
||||
public static var persistConnectionDelay: Double = 3
|
||||
|
||||
/// Default parameters that will be included in all requests unless explicitly overridden.
|
||||
public static var defaultParams: [String:Any] = [String: Any]()
|
||||
|
||||
/// Default HTTP headers for requests.
|
||||
///
|
||||
/// By default, it includes "Accept", "Content-Type", and "Accept-Encoding" headers.
|
||||
var defaultHeaders: [String: String] = ["Accept": "application/json",
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
"Accept-Encoding": "gzip"]
|
||||
|
||||
/// The shared singleton instance of the `API` struct.
|
||||
public static let shared = API()
|
||||
|
||||
private init(){}
|
||||
|
||||
/// Performs an asynchronous network request and decodes the response into a `Codable` type.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The URL string for the request.
|
||||
/// - params: Optional parameters for the request. Can be `[String: Any]` for JSON/form-data, or `Data` for raw body.
|
||||
/// - method: The HTTP method to use for the request (`.get`, `.post`, `.put`, `.delete`).
|
||||
/// - headers: Optional custom HTTP headers to be added to the request. These override default headers if there are conflicts.
|
||||
/// - jsonEncoding: A boolean indicating whether parameters should be JSON encoded. Defaults to `true`.
|
||||
/// - debug: A boolean indicating whether to print debug logs for the request and response. Defaults to `true`.
|
||||
/// - timeoutInterval: The timeout interval in seconds for the request. Defaults to `30`.
|
||||
/// - networkServiceType: The `URLRequest.NetworkServiceType` for the request. Defaults to `.default`.
|
||||
/// - persistConnection: A boolean indicating whether to persist the connection on certain error codes (e.g., 4xx). Defaults to `false`.
|
||||
/// - Returns: An instance of the `T` type, decoded from the response data.
|
||||
/// - Throws: An `Error` if the request fails, including `URLError` for network issues or `DecodingError` for JSON decoding failures.
|
||||
public func request<T: Codable>(url: String,
|
||||
params: Any? = nil,
|
||||
method: httpMethod,
|
||||
@@ -83,7 +118,7 @@ public struct API {
|
||||
|
||||
var body = Data()
|
||||
|
||||
// Adiciona campos adicionais (se houver)
|
||||
// Add additional fields (if any)
|
||||
for (key, value) in params where key != "file" {
|
||||
let stringValue = "\(value)"
|
||||
body.append("--\(boundary)\r\n".data(using: .utf8)!)
|
||||
@@ -91,7 +126,7 @@ public struct API {
|
||||
body.append("\(stringValue)\r\n".data(using: .utf8)!)
|
||||
}
|
||||
|
||||
// Adiciona o arquivo
|
||||
// Add the file
|
||||
let fileName = fileURL.lastPathComponent
|
||||
let mimeType = mimeTypeForPath(path: fileName)
|
||||
do {
|
||||
@@ -105,11 +140,11 @@ public struct API {
|
||||
printError(title: "Upload File", msg: error.localizedDescription)
|
||||
}
|
||||
|
||||
// Finaliza o corpo da requisição
|
||||
// Finalize the request body
|
||||
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
|
||||
request.httpBody = body
|
||||
request.setValue("\(body.count)", forHTTPHeaderField: "Content-Length")
|
||||
// Logs de depuração
|
||||
// Debug logs
|
||||
printLog(title: "Boundary", msg: boundary)
|
||||
if let bodyString = String(data: body, encoding: .utf8) {
|
||||
printLog(title: "Body Content", msg: bodyString)
|
||||
@@ -131,7 +166,7 @@ public struct API {
|
||||
request.timeoutInterval = timeoutInterval
|
||||
request.networkServiceType = networkServiceType
|
||||
|
||||
// - Put Default Headers togheter with user defined params
|
||||
// - Put Default Headers together with user defined params
|
||||
if !headers.isEmpty {
|
||||
// - Add it to request
|
||||
headers.forEach { (key, value) in
|
||||
@@ -187,6 +222,7 @@ public struct API {
|
||||
}
|
||||
if persistConnection {
|
||||
printError(title: "INTERNET CONNECTION ERROR", msg: "WILL PERSIST")
|
||||
// Recursive call for persistence
|
||||
let persist: T = try await self.request(
|
||||
url: url,
|
||||
params: params,
|
||||
@@ -221,6 +257,11 @@ public struct API {
|
||||
throw API.defaultError
|
||||
}
|
||||
|
||||
/// Sets up client certificate data and an optional password for authentication.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - certData: The `Data` representation of the client certificate (e.g., a .p12 file).
|
||||
/// - password: The password for the certificate, if required. Defaults to an empty string.
|
||||
public func setupCertificationRequest(certData: Data, password: String = "") {
|
||||
API.certData = certData
|
||||
API.certPassword = password
|
||||
@@ -228,6 +269,8 @@ public struct API {
|
||||
}
|
||||
|
||||
#if canImport(Security)
|
||||
/// A custom `URLSessionDelegate` handler for managing URL session challenges,
|
||||
/// particularly for client and server trust authentication.
|
||||
@available(iOS 13.0.0, *)
|
||||
@MainActor
|
||||
private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
|
||||
@@ -235,24 +278,38 @@ private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
|
||||
private var certData: Data?
|
||||
private var certPass: String?
|
||||
|
||||
/// Initializes a new `URLSessionDelegateHandler` instance.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - certData: Optional `Data` for the client certificate.
|
||||
/// - password: Optional password for the client certificate.
|
||||
init(certData: Data? = nil, password: String? = nil) {
|
||||
super.init()
|
||||
self.certData = certData
|
||||
self.certPass = password
|
||||
}
|
||||
|
||||
/// Handles URL session authentication challenges.
|
||||
///
|
||||
/// This method is responsible for providing client certificates for client certificate
|
||||
/// authentication and validating server trust for server authentication.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - session: The URL session that issued the challenge.
|
||||
/// - challenge: The authentication challenge.
|
||||
/// - Returns: A tuple containing the disposition for the challenge and the credential to use.
|
||||
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
|
||||
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
|
||||
// Carregar o certificado do cliente
|
||||
// Load the client certificate
|
||||
guard let identity = getIdentity() else {
|
||||
return (URLSession.AuthChallengeDisposition.performDefaultHandling, nil)
|
||||
}
|
||||
|
||||
// Criar o URLCredential com a identidade
|
||||
// Create the URLCredential with the identity
|
||||
let credential = URLCredential(identity: identity, certificates: nil, persistence: .forSession)
|
||||
return (URLSession.AuthChallengeDisposition.useCredential, credential)
|
||||
} else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
|
||||
// Validar o certificado do servidor
|
||||
// Validate the server certificate
|
||||
if let serverTrust = challenge.protectionSpace.serverTrust {
|
||||
let serverCredential = URLCredential(trust: serverTrust)
|
||||
return (URLSession.AuthChallengeDisposition.useCredential, serverCredential)
|
||||
@@ -264,13 +321,16 @@ private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the `SecIdentity` from the provided certificate data and password.
|
||||
///
|
||||
/// - Returns: A `SecIdentity` object if the certificate is successfully imported, otherwise `nil`.
|
||||
private func getIdentity() -> SecIdentity? {
|
||||
guard let certData = self.certData else { return nil }
|
||||
// Especifique a senha usada ao exportar o .p12
|
||||
// Specify the password used when exporting the .p12
|
||||
let options: [String: Any] = [kSecImportExportPassphrase as String: self.certPass ?? ""]
|
||||
var items: CFArray?
|
||||
|
||||
// Importar o certificado .p12 para obter a identidade
|
||||
// Import the .p12 certificate to get the identity
|
||||
let status = SecPKCS12Import(certData as CFData, options as CFDictionary, &items)
|
||||
|
||||
if status == errSecSuccess,
|
||||
@@ -285,6 +345,12 @@ private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
|
||||
|
||||
}
|
||||
|
||||
/// Checks if the provided data is a valid PKCS#12 (P12) certificate with the given password.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - data: The `Data` representation of the certificate.
|
||||
/// - password: The password for the certificate.
|
||||
/// - Returns: `true` if the data is a valid P12 certificate with the given password, otherwise `false`.
|
||||
private func isPKCS12(data: Data, password: String) -> Bool {
|
||||
let options: [String: Any] = [kSecImportExportPassphrase as String: password]
|
||||
|
||||
@@ -297,6 +363,7 @@ private class URLSessionDelegateHandler: NSObject, URLSessionDelegate {
|
||||
#endif
|
||||
|
||||
extension Error {
|
||||
/// Returns the status code of the error, if available.
|
||||
public var statusCode: Int {
|
||||
get{
|
||||
return self._code
|
||||
@@ -307,6 +374,11 @@ extension Error {
|
||||
@available(iOS 13.0.0, *)
|
||||
extension API {
|
||||
|
||||
/// Logs details of an outgoing network request for debugging purposes.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - method: The HTTP method of the request.
|
||||
/// - request: The `URLRequest` object.
|
||||
fileprivate static func requestLOG(method: httpMethod, request: URLRequest) {
|
||||
|
||||
print("\n<========================= 🟠 INTERNET CONNECTION - REQUEST =========================>")
|
||||
@@ -325,6 +397,14 @@ extension API {
|
||||
print("<======================================================================================>")
|
||||
}
|
||||
|
||||
/// Logs details of an incoming network response for debugging purposes.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - method: The HTTP method of the original request.
|
||||
/// - request: The `URLRequest` object that generated this response.
|
||||
/// - data: The data received in the response.
|
||||
/// - statusCode: The HTTP status code of the response.
|
||||
/// - error: An optional `Error` object if the request failed.
|
||||
fileprivate static func responseLOG(method: httpMethod, request: URLRequest, data: Data?, statusCode: Int, error: Error?) {
|
||||
///
|
||||
let icon = error != nil ? "🔴" : "🟢"
|
||||
@@ -384,11 +464,15 @@ extension API {
|
||||
print("<======================================================================================>")
|
||||
}
|
||||
|
||||
/// Determines the MIME type for a given file path based on its extension.
|
||||
///
|
||||
/// - Parameter path: The file path string.
|
||||
/// - Returns: A string representing the MIME type. Defaults to "application/octet-stream" if the type is unknown.
|
||||
func mimeTypeForPath(path: String) -> String {
|
||||
let url = URL(fileURLWithPath: path)
|
||||
let pathExtension = url.pathExtension.lowercased()
|
||||
|
||||
// Dicionário de extensões e MIME types comuns
|
||||
// Dictionary of common extensions and MIME types
|
||||
let mimeTypes: [String: String] = [
|
||||
"jpg": "image/jpeg",
|
||||
"jpeg": "image/jpeg",
|
||||
@@ -412,7 +496,7 @@ extension API {
|
||||
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
]
|
||||
|
||||
// Retorna o MIME type correspondente à extensão, ou "application/octet-stream" como padrão
|
||||
// Returns the corresponding MIME type for the extension, or "application/octet-stream" as default
|
||||
return mimeTypes[pathExtension] ?? "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
@@ -25,17 +25,29 @@ import UIKit
|
||||
import AVFoundation
|
||||
import Photos
|
||||
|
||||
/// A protocol that defines the methods an image picker delegate should implement.
|
||||
public protocol ImagePickerControllerDelegate: AnyObject {
|
||||
/// Tells the delegate that an image has been selected by the image picker.
|
||||
/// - Parameter image: The `UIImage` that was selected, or `nil` if the selection was canceled or failed.
|
||||
func imagePicker(didSelect image: UIImage?)
|
||||
}
|
||||
|
||||
/// A custom `UIViewController` that provides functionality for picking images from the camera or photo library.
|
||||
///
|
||||
/// This controller handles permissions for camera and photo library access and presents
|
||||
/// a `UIImagePickerController` to the user.
|
||||
public class ImagePickerController: UIViewController, UINavigationControllerDelegate {
|
||||
|
||||
private var isAlertOpen: Bool = false
|
||||
private var imagePickerController: UIImagePickerController = UIImagePickerController()
|
||||
|
||||
/// The delegate for the `ImagePickerController`, which will receive callbacks when an image is selected.
|
||||
public weak var delegate: ImagePickerControllerDelegate?
|
||||
|
||||
/// A boolean value that determines whether the user can edit the selected image. Defaults to `false`.
|
||||
public var isEditable: Bool = false
|
||||
|
||||
/// Initializes a new `ImagePickerController` instance.
|
||||
public init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
@@ -52,7 +64,10 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
|
||||
self.imagePickerController.mediaTypes = ["public.image", "public.movie"]
|
||||
}
|
||||
|
||||
|
||||
/// Presents the image picker to the user.
|
||||
///
|
||||
/// This method checks for camera and photo library permissions and then presents an alert
|
||||
/// allowing the user to choose between the camera or photo roll, or to grant permissions if needed.
|
||||
public func openImagePicker(){
|
||||
var cameraPerm: Bool = false
|
||||
var albumPerm: Bool = false
|
||||
@@ -85,6 +100,10 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
|
||||
}
|
||||
|
||||
|
||||
/// Presents an `UIAlertController` with options to open the camera, photo roll, or grant permissions.
|
||||
/// - Parameters:
|
||||
/// - forCamera: A boolean indicating whether camera access is granted. Defaults to `true`.
|
||||
/// - forAlbum: A boolean indicating whether photo album access is granted. Defaults to `true`.
|
||||
private func openAlerts(forCamera:Bool = true, forAlbum:Bool = true){
|
||||
let alert = UIAlertController(title: "Choose an option", message: nil, preferredStyle: .actionSheet)
|
||||
if forCamera {
|
||||
@@ -117,6 +136,9 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the camera device using `UIImagePickerController`.
|
||||
///
|
||||
/// If the camera is not available, an alert message is presented to the user.
|
||||
private func openCameraDevice(){
|
||||
if(UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera)){
|
||||
self.imagePickerController.sourceType = UIImagePickerController.SourceType.camera
|
||||
@@ -130,12 +152,14 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the photo library using `UIImagePickerController`.
|
||||
private func openAlbumDevice(){
|
||||
self.imagePickerController.sourceType = UIImagePickerController.SourceType.photoLibrary
|
||||
self.modalPresentationStyle = .fullScreen
|
||||
self.present(self.imagePickerController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
/// Requests photo library access and if denied, guides the user to app settings.
|
||||
private func openAppSettingsPhoto(){
|
||||
PHPhotoLibrary.requestAuthorization({status in
|
||||
if status == .authorized{
|
||||
@@ -160,6 +184,7 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
|
||||
})
|
||||
}
|
||||
|
||||
/// Requests camera access and if denied, guides the user to app settings.
|
||||
private func openAppSettingsCamera(){
|
||||
AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (granted: Bool) -> Void in
|
||||
if granted == true {
|
||||
@@ -189,6 +214,8 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele
|
||||
|
||||
extension ImagePickerController: UIImagePickerControllerDelegate {
|
||||
|
||||
/// Tells the delegate that the user canceled the pick operation.
|
||||
/// - Parameter picker: The image picker controller.
|
||||
@objc public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
self.delegate?.imagePicker(didSelect: nil)
|
||||
picker.dismiss(animated: true, completion: {
|
||||
@@ -196,6 +223,10 @@ extension ImagePickerController: UIImagePickerControllerDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
/// Tells the delegate that the user picked an image or movie.
|
||||
/// - Parameters:
|
||||
/// - picker: The image picker controller.
|
||||
/// - info: A dictionary containing the original image, and possibly an edited image or a movie URL.
|
||||
@objc public func imagePickerController(_ picker: UIImagePickerController,
|
||||
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||
if let image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
|
||||
|
@@ -22,13 +22,25 @@
|
||||
import UIKit
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
/// A protocol for delegates of `ImageZoomController` to provide callbacks for zoom and close events.
|
||||
@objc public protocol ImageZoomControllerDelegate {
|
||||
/// Called when the image in the controller is zoomed.
|
||||
/// - Parameters:
|
||||
/// - controller: The `ImageZoomController` instance.
|
||||
/// - image: The `UIImage` currently displayed and being zoomed.
|
||||
@objc optional func imageZoomController(controller: ImageZoomController, didZoom image: UIImage?)
|
||||
|
||||
/// Called when the image zoom controller is closed.
|
||||
/// - Parameters:
|
||||
/// - controller: The `ImageZoomController` instance.
|
||||
/// - image: The `UIImage` that was displayed when the controller was closed.
|
||||
@objc optional func imageZoomController(controller: ImageZoomController, didClose image: UIImage?)
|
||||
}
|
||||
|
||||
/// A `UIViewController` that allows users to zoom and pan an image, with an option to dismiss by dragging.
|
||||
public class ImageZoomController: UIViewController {
|
||||
|
||||
/// A private lazy `UIView` that serves as a black background with partial opacity.
|
||||
fileprivate lazy var blackView: UIView = {
|
||||
$0.isOpaque = true
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -37,6 +49,7 @@ public class ImageZoomController: UIViewController {
|
||||
return $0
|
||||
}(UIView())
|
||||
|
||||
/// A private lazy `UIScrollView` to enable zooming and panning of the image.
|
||||
fileprivate lazy var scrollView: UIScrollView = {
|
||||
$0.isOpaque = true
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -44,6 +57,7 @@ public class ImageZoomController: UIViewController {
|
||||
return $0
|
||||
}(UIScrollView())
|
||||
|
||||
/// A private lazy `UIButton` to close the image zoom controller.
|
||||
fileprivate lazy var closeButton: UIButton = {
|
||||
$0.isOpaque = true
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -55,6 +69,7 @@ public class ImageZoomController: UIViewController {
|
||||
return $0
|
||||
}(UIButton(type: .custom))
|
||||
|
||||
/// The `UIImageView` that displays the image.
|
||||
lazy var imageView: UIImageView = {
|
||||
$0.isOpaque = true
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -62,16 +77,24 @@ public class ImageZoomController: UIViewController {
|
||||
return $0
|
||||
}(UIImageView())
|
||||
|
||||
/// The minimum zoom scale allowed for the image. Defaults to `1.0`.
|
||||
public var minimumZoomScale: CGFloat = 1.0
|
||||
|
||||
/// The maximum zoom scale allowed for the image. Defaults to `6.0`.
|
||||
public var maximumZoomScale: CGFloat = 6.0
|
||||
|
||||
/// A boolean value indicating whether a pan gesture can be used to dismiss the controller. Defaults to `true`.
|
||||
public var addGestureToDismiss: Bool = true
|
||||
|
||||
/// The delegate for the `ImageZoomController`.
|
||||
public weak var delegate: ImageZoomControllerDelegate?
|
||||
|
||||
private var minimumVelocityToHide: CGFloat = 1500
|
||||
private var minimumScreenRatioToHide: CGFloat = 0.5
|
||||
private var animationDuration: TimeInterval = 0.2
|
||||
|
||||
|
||||
/// Initializes a new `ImageZoomController` with a specified image.
|
||||
/// - Parameter withImage: The `UIImage` to be displayed and zoomed.
|
||||
public init(_ withImage: UIImage) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
@@ -101,6 +124,7 @@ public class ImageZoomController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the subviews and sets up their constraints.
|
||||
fileprivate func addComponentsAndConstraints() {
|
||||
|
||||
scrollView.addSubview(imageView, translatesAutoresizingMaskIntoConstraints: false)
|
||||
@@ -124,6 +148,8 @@ public class ImageZoomController: UIViewController {
|
||||
.setHeight(size: 50.0)
|
||||
}
|
||||
|
||||
/// Presents the `ImageZoomController` from the top-most view controller.
|
||||
/// - Parameter completion: An optional closure to be executed after the presentation is complete.
|
||||
public func present(completion: (()->())? = nil) {
|
||||
guard let viewController = LCEssentials.getTopViewController(aboveBars: true) else {
|
||||
fatalError("Ops! Look like it doesnt have a ViewController")
|
||||
@@ -135,21 +161,29 @@ public class ImageZoomController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismisses the `ImageZoomController`.
|
||||
/// - Parameter completion: An optional closure to be executed after the dismissal is complete.
|
||||
public func dismiss(completion: (()->())? = nil) {
|
||||
self.dismiss(animated: true) {
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
/// Action method for the close button.
|
||||
/// Notifies the delegate that the controller is closing and then dismisses itself.
|
||||
@objc private func close(){
|
||||
delegate?.imageZoomController?(controller: self, didClose: self.imageView.image)
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
/// Slides the view vertically to a specified Y-coordinate.
|
||||
/// - Parameter y: The Y-coordinate to slide the view to.
|
||||
private func slideViewVerticallyTo(_ y: CGFloat) {
|
||||
self.view.frame.origin = CGPoint(x: 0, y: y)
|
||||
}
|
||||
|
||||
/// Handles pan gestures for dismissing the view.
|
||||
/// - Parameter panGesture: The `UIPanGestureRecognizer` instance.
|
||||
@objc private func onPan(_ panGesture: UIPanGestureRecognizer) {
|
||||
|
||||
switch panGesture.state {
|
||||
@@ -194,10 +228,15 @@ public class ImageZoomController: UIViewController {
|
||||
|
||||
extension ImageZoomController: UIScrollViewDelegate {
|
||||
|
||||
/// Tells the delegate that the scroll view has zoomed.
|
||||
/// - Parameter scrollView: The scroll view that zoomed.
|
||||
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||
delegate?.imageZoomController?(controller: self, didZoom: self.imageView.image)
|
||||
}
|
||||
|
||||
/// Returns the view that will be zoomed when the scroll view is zoomed.
|
||||
/// - Parameter scrollView: The scroll view that is zooming.
|
||||
/// - Returns: The view to be zoomed, which is the `imageView` in this case.
|
||||
public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
return self.imageView
|
||||
}
|
||||
|
@@ -24,10 +24,19 @@ import UIKit
|
||||
|
||||
// MARK: - Protocols
|
||||
|
||||
/// A protocol that defines optional methods for `LCSnackBarView` delegate.
|
||||
@objc
|
||||
public protocol LCSnackBarViewDelegate {
|
||||
/// Called when the snackbar starts its exhibition.
|
||||
/// - Parameter didStartExibition: The `LCSnackBarView` instance that started exhibition.
|
||||
@objc optional func snackbar(didStartExibition: LCSnackBarView)
|
||||
|
||||
/// Called when the snackbar is touched.
|
||||
/// - Parameter snackbar: The `LCSnackBarView` instance that was touched.
|
||||
@objc optional func snackbar(didTouchOn snackbar: LCSnackBarView)
|
||||
|
||||
/// Called when the snackbar ends its exhibition.
|
||||
/// - Parameter didEndExibition: The `LCSnackBarView` instance that ended exhibition.
|
||||
@objc optional func snackbar(didEndExibition: LCSnackBarView)
|
||||
}
|
||||
|
||||
@@ -36,23 +45,36 @@ public protocol LCSnackBarViewDelegate {
|
||||
|
||||
// MARK: - Local Defines / ENUMS
|
||||
|
||||
/// Enumeration defining the visual style of the `LCSnackBarView`.
|
||||
public enum LCSnackBarViewType {
|
||||
case `default`, rounded
|
||||
/// The default style, typically rectangular.
|
||||
case `default`
|
||||
/// A rounded style for the snackbar.
|
||||
case rounded
|
||||
}
|
||||
|
||||
/// Enumeration defining the orientation of the `LCSnackBarView`.
|
||||
public enum LCSnackBarOrientation {
|
||||
case top, bottom
|
||||
/// The snackbar appears at the top of the screen.
|
||||
case top
|
||||
/// The snackbar appears at the bottom of the screen.
|
||||
case bottom
|
||||
}
|
||||
|
||||
/// Enumeration defining the display duration for the `LCSnackBarView`.
|
||||
public enum LCSnackBarTimer: CGFloat {
|
||||
/// The snackbar remains visible indefinitely until dismissed manually.
|
||||
case infinity = 0
|
||||
/// Minimum display duration (2 seconds).
|
||||
case minimum = 2
|
||||
/// Medium display duration (5 seconds).
|
||||
case medium = 5
|
||||
/// Maximum display duration (10 seconds).
|
||||
case maximum = 10
|
||||
}
|
||||
|
||||
// MARK: - Class
|
||||
/// LCSnackBarView is a simple SnackBar that you can display notifications in app to improve your app comunication
|
||||
/// `LCSnackBarView` is a simple SnackBar that you can display notifications in-app to improve your app communication.
|
||||
///
|
||||
/// Usage example:
|
||||
///
|
||||
@@ -62,7 +84,7 @@ public enum LCSnackBarTimer: CGFloat {
|
||||
/// .configure(text: "Hello World!")
|
||||
/// .present()
|
||||
///```
|
||||
///You can set delegate to interact with it
|
||||
///You can set a delegate to interact with it:
|
||||
///
|
||||
///```swift
|
||||
///let notification = LCSnackBarView(delegate: self)
|
||||
@@ -78,6 +100,7 @@ public final class LCSnackBarView: UIView {
|
||||
|
||||
// MARK: - Private properties
|
||||
|
||||
/// The content view that holds the snackbar's elements.
|
||||
private lazy var contentView: UIView = {
|
||||
$0.backgroundColor = .white
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -85,6 +108,7 @@ public final class LCSnackBarView: UIView {
|
||||
return $0
|
||||
}(UIView())
|
||||
|
||||
/// The label that displays the main text of the snackbar.
|
||||
private lazy var descriptionLabel: UILabel = {
|
||||
$0.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
$0.text = nil
|
||||
@@ -117,10 +141,16 @@ public final class LCSnackBarView: UIView {
|
||||
|
||||
// MARK: - Public properties
|
||||
|
||||
/// The delegate for the snackbar view.
|
||||
public weak var delegate: LCSnackBarViewDelegate?
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
/// Initializes a new `LCSnackBarView` instance.
|
||||
/// - Parameters:
|
||||
/// - style: The visual style of the snackbar. Defaults to `.default`.
|
||||
/// - orientation: The orientation (top or bottom) of the snackbar. Defaults to `.top`.
|
||||
/// - delegate: The delegate to receive snackbar events. Defaults to `nil`.
|
||||
public init(
|
||||
style: LCSnackBarViewType = .default,
|
||||
orientation: LCSnackBarOrientation = .top,
|
||||
@@ -161,12 +191,14 @@ public extension LCSnackBarView {
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
/// Sets up the default layout properties for the snackbar.
|
||||
private func setupDefaultLayout() {
|
||||
backgroundColor = .white
|
||||
contentView.backgroundColor = .white
|
||||
clipsToBounds = true
|
||||
}
|
||||
|
||||
/// Sets up a tap gesture recognizer for the snackbar.
|
||||
private func setupGestureRecognizer() {
|
||||
let gesture = UITapGestureRecognizer(target: self, action: #selector(onTapGestureAction))
|
||||
gesture.numberOfTapsRequired = 1
|
||||
@@ -174,6 +206,7 @@ public extension LCSnackBarView {
|
||||
addGestureRecognizer(gesture)
|
||||
}
|
||||
|
||||
/// Configures observers for keyboard appearance and disappearance notifications.
|
||||
private func setKeyboardObserver() {
|
||||
// Show
|
||||
NotificationCenter
|
||||
@@ -196,6 +229,8 @@ public extension LCSnackBarView {
|
||||
)
|
||||
}
|
||||
|
||||
/// Handles the `keyboardWillShowNotification` to adjust the snackbar's position.
|
||||
/// - Parameter notification: The `Notification` object containing keyboard information.
|
||||
@objc private func keyboardWillShow(_ notification: Notification?) -> Void {
|
||||
|
||||
if let info = notification?.userInfo {
|
||||
@@ -236,6 +271,8 @@ public extension LCSnackBarView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the `keyboardWillHideNotification`.
|
||||
/// - Parameter notification: The `Notification` object.
|
||||
@objc private func keyboardWillHide(_ notification: Notification?) -> Void {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.systemKeyboardVisible = false
|
||||
@@ -243,6 +280,7 @@ public extension LCSnackBarView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the snackbar's style properties, such as width and corner radius, based on `_style`.
|
||||
private func updateStyle() {
|
||||
switch _style {
|
||||
case .rounded:
|
||||
@@ -256,6 +294,8 @@ public extension LCSnackBarView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Positions the snackbar view within the given superview based on its `_orientation`.
|
||||
/// - Parameter view: The `UIView` that will contain the snackbar.
|
||||
private func positioningView(_ view: UIView) {
|
||||
view
|
||||
.addSubview(self,
|
||||
@@ -286,6 +326,10 @@ public extension LCSnackBarView {
|
||||
height: _height)
|
||||
}
|
||||
|
||||
/// Displays the snackbar with an animation.
|
||||
/// - Parameters:
|
||||
/// - controller: The `UIViewController` on which the snackbar will be presented.
|
||||
/// - completion: A closure to be executed once the presentation animation completes.
|
||||
private func showSnackBar(controller: UIViewController, completion: @escaping (() -> Void)) {
|
||||
if isOpen {
|
||||
closeSnackBar(controller: controller) {
|
||||
@@ -320,6 +364,10 @@ public extension LCSnackBarView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hides the snackbar with an animation.
|
||||
/// - Parameters:
|
||||
/// - controller: The `UIViewController` from which the snackbar is being dismissed.
|
||||
/// - completion: A closure to be executed once the dismissal animation completes.
|
||||
private func closeSnackBar(controller: UIViewController, completion: @escaping (() -> Void)) {
|
||||
let distance = CGFloat(_style == .rounded ? (_orientation == .top ? UIDevice.topNotch : UIDevice.bottomNotch) : 0)
|
||||
layoutIfNeeded()
|
||||
@@ -341,6 +389,8 @@ public extension LCSnackBarView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles tap gestures on the snackbar.
|
||||
/// - Parameter _: The `UITapGestureRecognizer` instance.
|
||||
@objc
|
||||
private func onTapGestureAction(_ : UITapGestureRecognizer) {
|
||||
self.delegate?.snackbar?(didTouchOn: self)
|
||||
@@ -350,6 +400,7 @@ public extension LCSnackBarView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the subviews to the snackbar and sets up their constraints.
|
||||
private func addComponentsAndConstraints() {
|
||||
|
||||
// MARK: - Add Subviews
|
||||
@@ -376,18 +427,29 @@ public extension LCSnackBarView {
|
||||
|
||||
// MARK: - Public methods
|
||||
|
||||
/// Configures the text displayed in the snackbar.
|
||||
/// - Parameter text: The `String` text to display.
|
||||
/// - Returns: The `LCSnackBarView` instance for chaining.
|
||||
@discardableResult
|
||||
func configure(text: String) -> Self {
|
||||
descriptionLabel.text = text
|
||||
return self
|
||||
}
|
||||
|
||||
/// Configures the color of the text in the snackbar.
|
||||
/// - Parameter textColor: The `UIColor` for the text.
|
||||
/// - Returns: The `LCSnackBarView` instance for chaining.
|
||||
@discardableResult
|
||||
func configure(textColor: UIColor) -> Self {
|
||||
descriptionLabel.textColor = textColor
|
||||
return self
|
||||
}
|
||||
|
||||
/// Configures the font and text alignment of the snackbar's text.
|
||||
/// - Parameters:
|
||||
/// - textFont: The `UIFont` for the text.
|
||||
/// - alignment: The `NSTextAlignment` for the text. Defaults to `.center`.
|
||||
/// - Returns: The `LCSnackBarView` instance for chaining.
|
||||
@discardableResult
|
||||
func configure(textFont: UIFont, alignment: NSTextAlignment = .center) -> Self {
|
||||
descriptionLabel.font = textFont
|
||||
@@ -395,6 +457,9 @@ public extension LCSnackBarView {
|
||||
return self
|
||||
}
|
||||
|
||||
/// Configures the background color of the snackbar.
|
||||
/// - Parameter backgroundColor: The `UIColor` for the background.
|
||||
/// - Returns: The `LCSnackBarView` instance for chaining.
|
||||
@discardableResult
|
||||
func configure(backgroundColor: UIColor) -> Self {
|
||||
self.backgroundColor = backgroundColor
|
||||
@@ -402,12 +467,20 @@ public extension LCSnackBarView {
|
||||
return self
|
||||
}
|
||||
|
||||
/// Configures the exhibition duration (timer) of the snackbar.
|
||||
/// - Parameter timer: The `LCSnackBarTimer` value.
|
||||
/// - Returns: The `LCSnackBarView` instance for chaining.
|
||||
@discardableResult
|
||||
func configure(exibition timer: LCSnackBarTimer) -> Self {
|
||||
_timer = timer
|
||||
return self
|
||||
}
|
||||
|
||||
/// Configures an image icon to be displayed before the text in the snackbar.
|
||||
/// - Parameters:
|
||||
/// - icon: The `UIImageView` to use as the icon.
|
||||
/// - withTintColor: An optional `UIColor` to tint the icon. Defaults to `nil`.
|
||||
/// - Returns: The `LCSnackBarView` instance for chaining.
|
||||
@discardableResult
|
||||
func configure(imageIconBefore icon: UIImageView, withTintColor: UIColor? = nil) -> Self {
|
||||
icon.setHeight(size: 24)
|
||||
@@ -428,6 +501,8 @@ public extension LCSnackBarView {
|
||||
return self
|
||||
}
|
||||
|
||||
/// Presents the snackbar on the top-most view controller.
|
||||
/// - Parameter completion: An optional closure to be executed after the presentation.
|
||||
func present(completion: (()->())? = nil) {
|
||||
if isOpen { return }
|
||||
if let controller = LCEssentials.getTopViewController(aboveBars: true) {
|
||||
|
@@ -21,28 +21,49 @@
|
||||
#if canImport(SwiftUI)
|
||||
import SwiftUI
|
||||
|
||||
/// `LCENavigationState` is an `ObservableObject` that manages the state for `LCENavigationView`.
|
||||
/// It includes published properties for managing button images, text, actions,
|
||||
/// navigation bar visibility, and title/subtitle views.
|
||||
@available(iOS 15, *)
|
||||
class LCENavigationState: ObservableObject {
|
||||
/// The image view for the right button.
|
||||
@Published var rightButtonImage: AnyView? = nil
|
||||
/// The text for the right button.
|
||||
@Published var rightButtonText: Text = Text("")
|
||||
/// The action to perform when the right button is tapped.
|
||||
@Published var rightButtonAction: () -> Void = {}
|
||||
|
||||
/// The image view for the left button.
|
||||
@Published var leftButtonImage: AnyView? = nil
|
||||
/// The text for the left button.
|
||||
@Published var leftButtonText: Text = Text("")
|
||||
/// The action to perform when the left button is tapped.
|
||||
@Published var leftButtonAction: () -> Void = {}
|
||||
|
||||
/// A boolean value that controls the visibility of the navigation bar.
|
||||
@Published var hideNavigationBar: Bool = false
|
||||
|
||||
/// The title view of the navigation bar.
|
||||
@Published var title: (any View) = Text("")
|
||||
/// The subtitle view of the navigation bar.
|
||||
@Published var subTitle: (any View) = Text("")
|
||||
}
|
||||
|
||||
/// `LCENavigationView` is a SwiftUI `View` that provides a customizable navigation bar.
|
||||
/// It allows setting left and right buttons, a title, and a subtitle.
|
||||
@available(iOS 15, *)
|
||||
public struct LCENavigationView<Content: View>: View {
|
||||
/// The observed state object for the navigation view.
|
||||
@ObservedObject private var state: LCENavigationState
|
||||
|
||||
/// The content view displayed below the navigation bar.
|
||||
let content: Content
|
||||
|
||||
/// Initializes a new `LCENavigationView` instance.
|
||||
/// - Parameters:
|
||||
/// - title: The title view for the navigation bar. Defaults to an empty `Text`.
|
||||
/// - subTitle: The subtitle view for the navigation bar. Defaults to an empty `Text`.
|
||||
/// - content: A `ViewBuilder` that provides the content to be displayed below the navigation bar.
|
||||
public init(
|
||||
title: (any View) = Text(""),
|
||||
subTitle: (any View) = Text(""),
|
||||
@@ -56,6 +77,7 @@ public struct LCENavigationView<Content: View>: View {
|
||||
self.state.subTitle = subTitle
|
||||
}
|
||||
|
||||
/// The body of the `LCENavigationView`.
|
||||
public var body: some View {
|
||||
VStack {
|
||||
if !state.hideNavigationBar {
|
||||
@@ -66,6 +88,7 @@ public struct LCENavigationView<Content: View>: View {
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
|
||||
/// The private `NavigationBarView` that lays out the navigation bar components.
|
||||
private var NavigationBarView: some View {
|
||||
HStack {
|
||||
NavLeftButton
|
||||
@@ -81,6 +104,7 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The private `TitleView` that displays the title and subtitle.
|
||||
private var TitleView: some View {
|
||||
VStack {
|
||||
AnyView(state.title)
|
||||
@@ -90,6 +114,7 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The private `NavLeftButton` view.
|
||||
private var NavLeftButton: some View {
|
||||
Button(action: state.leftButtonAction) {
|
||||
HStack {
|
||||
@@ -101,6 +126,7 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The private `NavRightButton` view.
|
||||
private var NavRightButton: some View {
|
||||
Button(action: state.rightButtonAction) {
|
||||
HStack {
|
||||
@@ -112,6 +138,12 @@ public struct LCENavigationView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the configuration for the right button of the navigation bar.
|
||||
/// - Parameters:
|
||||
/// - text: The `Text` to display on the button. Defaults to an empty `Text`.
|
||||
/// - image: An optional `View` to use as the button's icon. Defaults to `nil`.
|
||||
/// - action: The closure to execute when the button is tapped.
|
||||
/// - Returns: The `LCENavigationView` instance for chaining.
|
||||
public func setRightButton(
|
||||
text: Text = Text(""),
|
||||
image: (any View)? = nil,
|
||||
@@ -135,6 +167,12 @@ public struct LCENavigationView<Content: View>: View {
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the configuration for the left button of the navigation bar.
|
||||
/// - Parameters:
|
||||
/// - text: The `Text` to display on the button. Defaults to an empty `Text`.
|
||||
/// - image: An optional `View` to use as the button's icon. Defaults to `nil`.
|
||||
/// - action: The closure to execute when the button is tapped.
|
||||
/// - Returns: The `LCENavigationView` instance for chaining.
|
||||
public func setLeftButton(
|
||||
text: Text = Text(""),
|
||||
image: (any View)? = nil,
|
||||
@@ -158,6 +196,11 @@ public struct LCENavigationView<Content: View>: View {
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the title and optional subtitle for the navigation bar.
|
||||
/// - Parameters:
|
||||
/// - text: The `View` to use as the main title. Defaults to an empty `Text`.
|
||||
/// - subTitle: An optional `View` to use as the subtitle. Defaults to `nil`.
|
||||
/// - Returns: The `LCENavigationView` instance for chaining.
|
||||
public func setTitle(
|
||||
text: (any View) = Text(""),
|
||||
subTitle: (any View)? = nil
|
||||
@@ -167,14 +210,21 @@ public struct LCENavigationView<Content: View>: View {
|
||||
return self
|
||||
}
|
||||
|
||||
/// Controls the visibility of the navigation bar.
|
||||
/// - Parameter hide: A boolean value indicating whether to hide (`true`) or show (`false`) the navigation bar.
|
||||
/// - Returns: The `LCENavigationView` instance for chaining.
|
||||
public func hideNavigationView(_ hide: Bool) -> LCENavigationView {
|
||||
state.hideNavigationBar = hide
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension to `FormatStyle` to format any value as a string.
|
||||
@available(iOS 15.0, *)
|
||||
extension FormatStyle {
|
||||
/// Formats an input value if it matches the `FormatInput` type.
|
||||
/// - Parameter value: The value to format as `Any`.
|
||||
/// - Returns: The formatted output, or `nil` if the value type does not match.
|
||||
func format(any value: Any) -> FormatOutput? {
|
||||
if let v = value as? FormatInput {
|
||||
return format(v)
|
||||
@@ -183,8 +233,11 @@ extension FormatStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension to `LocalizedStringKey` to resolve localized strings.
|
||||
@available(iOS 15.0, *)
|
||||
extension LocalizedStringKey {
|
||||
/// Resolves the localized string key into a `String`.
|
||||
/// - Returns: The resolved string, or `nil` if resolution fails.
|
||||
var resolved: String? {
|
||||
let mirror = Mirror(reflecting: self)
|
||||
guard let key = mirror.descendant("key") as? String else {
|
||||
@@ -237,8 +290,11 @@ extension LocalizedStringKey {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension to `Text` to retrieve its string content.
|
||||
@available(iOS 15.0, *)
|
||||
extension Text {
|
||||
/// Returns the string representation of the `Text` view.
|
||||
/// - Returns: The string content, or `nil` if it cannot be extracted.
|
||||
var string: String? {
|
||||
let mirror = Mirror(reflecting: self)
|
||||
if let s = mirror.descendant("storage", "verbatim") as? String {
|
||||
|
Reference in New Issue
Block a user