From 0910973e9a675cca79ad546539823a42e3b09ef3 Mon Sep 17 00:00:00 2001 From: Daniel Arantes Loverde Date: Mon, 23 Jun 2025 09:48:57 -0300 Subject: [PATCH] documentation --- .../Classes/LCEssentials+API.swift | 110 +++++++++++++++--- .../ImagePicker/ImagePickerController.swift | 35 +++++- .../LCEssentials/ImageZoom/ImageZoom.swift | 43 ++++++- .../LCEssentials/Message/LCSnackBarView.swift | 83 ++++++++++++- .../SwiftUI/LCENavigationView.swift | 58 ++++++++- 5 files changed, 307 insertions(+), 22 deletions(-) diff --git a/Sources/LCEssentials/Classes/LCEssentials+API.swift b/Sources/LCEssentials/Classes/LCEssentials+API.swift index 27193bc..88bf744 100644 --- a/Sources/LCEssentials/Classes/LCEssentials+API.swift +++ b/Sources/LCEssentials/Classes/LCEssentials+API.swift @@ -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 { + /// 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(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" } } diff --git a/Sources/LCEssentials/ImagePicker/ImagePickerController.swift b/Sources/LCEssentials/ImagePicker/ImagePickerController.swift index 2327d5b..c627beb 100644 --- a/Sources/LCEssentials/ImagePicker/ImagePickerController.swift +++ b/Sources/LCEssentials/ImagePicker/ImagePickerController.swift @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2023 Loverde Co. // // Permission is hereby granted, free of charge, to any person obtaining a copy @@ -25,17 +25,29 @@ import UIKit import AVFoundation import Photos +/// A protocol that defines the methods an image picker delegate should implement. public protocol ImagePickerControllerDelegate: AnyObject { + /// Tells the delegate that an image has been selected by the image picker. + /// - Parameter image: The `UIImage` that was selected, or `nil` if the selection was canceled or failed. func imagePicker(didSelect image: UIImage?) } +/// A custom `UIViewController` that provides functionality for picking images from the camera or photo library. +/// +/// This controller handles permissions for camera and photo library access and presents +/// a `UIImagePickerController` to the user. public class ImagePickerController: UIViewController, UINavigationControllerDelegate { private var isAlertOpen: Bool = false private var imagePickerController: UIImagePickerController = UIImagePickerController() + + /// The delegate for the `ImagePickerController`, which will receive callbacks when an image is selected. public weak var delegate: ImagePickerControllerDelegate? + + /// A boolean value that determines whether the user can edit the selected image. Defaults to `false`. public var isEditable: Bool = false + /// Initializes a new `ImagePickerController` instance. public init() { super.init(nibName: nil, bundle: nil) } @@ -52,7 +64,10 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele self.imagePickerController.mediaTypes = ["public.image", "public.movie"] } - + /// Presents the image picker to the user. + /// + /// This method checks for camera and photo library permissions and then presents an alert + /// allowing the user to choose between the camera or photo roll, or to grant permissions if needed. public func openImagePicker(){ var cameraPerm: Bool = false var albumPerm: Bool = false @@ -85,6 +100,10 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele } + /// Presents an `UIAlertController` with options to open the camera, photo roll, or grant permissions. + /// - Parameters: + /// - forCamera: A boolean indicating whether camera access is granted. Defaults to `true`. + /// - forAlbum: A boolean indicating whether photo album access is granted. Defaults to `true`. private func openAlerts(forCamera:Bool = true, forAlbum:Bool = true){ let alert = UIAlertController(title: "Choose an option", message: nil, preferredStyle: .actionSheet) if forCamera { @@ -117,6 +136,9 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele } } + /// Opens the camera device using `UIImagePickerController`. + /// + /// If the camera is not available, an alert message is presented to the user. private func openCameraDevice(){ if(UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera)){ self.imagePickerController.sourceType = UIImagePickerController.SourceType.camera @@ -130,12 +152,14 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele } } + /// Opens the photo library using `UIImagePickerController`. private func openAlbumDevice(){ self.imagePickerController.sourceType = UIImagePickerController.SourceType.photoLibrary self.modalPresentationStyle = .fullScreen self.present(self.imagePickerController, animated: true, completion: nil) } + /// Requests photo library access and if denied, guides the user to app settings. private func openAppSettingsPhoto(){ PHPhotoLibrary.requestAuthorization({status in if status == .authorized{ @@ -160,6 +184,7 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele }) } + /// Requests camera access and if denied, guides the user to app settings. private func openAppSettingsCamera(){ AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (granted: Bool) -> Void in if granted == true { @@ -189,6 +214,8 @@ public class ImagePickerController: UIViewController, UINavigationControllerDele extension ImagePickerController: UIImagePickerControllerDelegate { + /// Tells the delegate that the user canceled the pick operation. + /// - Parameter picker: The image picker controller. @objc public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { self.delegate?.imagePicker(didSelect: nil) picker.dismiss(animated: true, completion: { @@ -196,6 +223,10 @@ extension ImagePickerController: UIImagePickerControllerDelegate { }) } + /// Tells the delegate that the user picked an image or movie. + /// - Parameters: + /// - picker: The image picker controller. + /// - info: A dictionary containing the original image, and possibly an edited image or a movie URL. @objc public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { if let image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage { diff --git a/Sources/LCEssentials/ImageZoom/ImageZoom.swift b/Sources/LCEssentials/ImageZoom/ImageZoom.swift index 0b8a2cb..83f9115 100644 --- a/Sources/LCEssentials/ImageZoom/ImageZoom.swift +++ b/Sources/LCEssentials/ImageZoom/ImageZoom.swift @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2023 Loverde Co. // // Permission is hereby granted, free of charge, to any person obtaining a copy @@ -22,13 +22,25 @@ import UIKit #if os(iOS) || os(macOS) +/// A protocol for delegates of `ImageZoomController` to provide callbacks for zoom and close events. @objc public protocol ImageZoomControllerDelegate { + /// Called when the image in the controller is zoomed. + /// - Parameters: + /// - controller: The `ImageZoomController` instance. + /// - image: The `UIImage` currently displayed and being zoomed. @objc optional func imageZoomController(controller: ImageZoomController, didZoom image: UIImage?) + + /// Called when the image zoom controller is closed. + /// - Parameters: + /// - controller: The `ImageZoomController` instance. + /// - image: The `UIImage` that was displayed when the controller was closed. @objc optional func imageZoomController(controller: ImageZoomController, didClose image: UIImage?) } +/// A `UIViewController` that allows users to zoom and pan an image, with an option to dismiss by dragging. public class ImageZoomController: UIViewController { + /// A private lazy `UIView` that serves as a black background with partial opacity. fileprivate lazy var blackView: UIView = { $0.isOpaque = true $0.translatesAutoresizingMaskIntoConstraints = false @@ -37,6 +49,7 @@ public class ImageZoomController: UIViewController { return $0 }(UIView()) + /// A private lazy `UIScrollView` to enable zooming and panning of the image. fileprivate lazy var scrollView: UIScrollView = { $0.isOpaque = true $0.translatesAutoresizingMaskIntoConstraints = false @@ -44,6 +57,7 @@ public class ImageZoomController: UIViewController { return $0 }(UIScrollView()) + /// A private lazy `UIButton` to close the image zoom controller. fileprivate lazy var closeButton: UIButton = { $0.isOpaque = true $0.translatesAutoresizingMaskIntoConstraints = false @@ -55,6 +69,7 @@ public class ImageZoomController: UIViewController { return $0 }(UIButton(type: .custom)) + /// The `UIImageView` that displays the image. lazy var imageView: UIImageView = { $0.isOpaque = true $0.translatesAutoresizingMaskIntoConstraints = false @@ -62,16 +77,24 @@ public class ImageZoomController: UIViewController { return $0 }(UIImageView()) + /// The minimum zoom scale allowed for the image. Defaults to `1.0`. public var minimumZoomScale: CGFloat = 1.0 + + /// The maximum zoom scale allowed for the image. Defaults to `6.0`. public var maximumZoomScale: CGFloat = 6.0 + + /// A boolean value indicating whether a pan gesture can be used to dismiss the controller. Defaults to `true`. public var addGestureToDismiss: Bool = true + + /// The delegate for the `ImageZoomController`. public weak var delegate: ImageZoomControllerDelegate? private var minimumVelocityToHide: CGFloat = 1500 private var minimumScreenRatioToHide: CGFloat = 0.5 private var animationDuration: TimeInterval = 0.2 - + /// Initializes a new `ImageZoomController` with a specified image. + /// - Parameter withImage: The `UIImage` to be displayed and zoomed. public init(_ withImage: UIImage) { super.init(nibName: nil, bundle: nil) @@ -101,6 +124,7 @@ public class ImageZoomController: UIViewController { } } + /// Adds the subviews and sets up their constraints. fileprivate func addComponentsAndConstraints() { scrollView.addSubview(imageView, translatesAutoresizingMaskIntoConstraints: false) @@ -124,6 +148,8 @@ public class ImageZoomController: UIViewController { .setHeight(size: 50.0) } + /// Presents the `ImageZoomController` from the top-most view controller. + /// - Parameter completion: An optional closure to be executed after the presentation is complete. public func present(completion: (()->())? = nil) { guard let viewController = LCEssentials.getTopViewController(aboveBars: true) else { fatalError("Ops! Look like it doesnt have a ViewController") @@ -135,21 +161,29 @@ public class ImageZoomController: UIViewController { } } + /// Dismisses the `ImageZoomController`. + /// - Parameter completion: An optional closure to be executed after the dismissal is complete. public func dismiss(completion: (()->())? = nil) { self.dismiss(animated: true) { completion?() } } + /// Action method for the close button. + /// Notifies the delegate that the controller is closing and then dismisses itself. @objc private func close(){ delegate?.imageZoomController?(controller: self, didClose: self.imageView.image) self.dismiss() } + /// Slides the view vertically to a specified Y-coordinate. + /// - Parameter y: The Y-coordinate to slide the view to. private func slideViewVerticallyTo(_ y: CGFloat) { self.view.frame.origin = CGPoint(x: 0, y: y) } + /// Handles pan gestures for dismissing the view. + /// - Parameter panGesture: The `UIPanGestureRecognizer` instance. @objc private func onPan(_ panGesture: UIPanGestureRecognizer) { switch panGesture.state { @@ -194,10 +228,15 @@ public class ImageZoomController: UIViewController { extension ImageZoomController: UIScrollViewDelegate { + /// Tells the delegate that the scroll view has zoomed. + /// - Parameter scrollView: The scroll view that zoomed. public func scrollViewDidZoom(_ scrollView: UIScrollView) { delegate?.imageZoomController?(controller: self, didZoom: self.imageView.image) } + /// Returns the view that will be zoomed when the scroll view is zoomed. + /// - Parameter scrollView: The scroll view that is zooming. + /// - Returns: The view to be zoomed, which is the `imageView` in this case. public func viewForZooming(in scrollView: UIScrollView) -> UIView? { return self.imageView } diff --git a/Sources/LCEssentials/Message/LCSnackBarView.swift b/Sources/LCEssentials/Message/LCSnackBarView.swift index 387ef64..669a730 100644 --- a/Sources/LCEssentials/Message/LCSnackBarView.swift +++ b/Sources/LCEssentials/Message/LCSnackBarView.swift @@ -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) { diff --git a/Sources/LCEssentials/SwiftUI/LCENavigationView.swift b/Sources/LCEssentials/SwiftUI/LCENavigationView.swift index c157694..da1b2e2 100644 --- a/Sources/LCEssentials/SwiftUI/LCENavigationView.swift +++ b/Sources/LCEssentials/SwiftUI/LCENavigationView.swift @@ -1,4 +1,4 @@ -// +// // Copyright (c) 2024 Loverde Co. // // Permission is hereby granted, free of charge, to any person obtaining a copy @@ -21,28 +21,49 @@ #if canImport(SwiftUI) import SwiftUI +/// `LCENavigationState` is an `ObservableObject` that manages the state for `LCENavigationView`. +/// It includes published properties for managing button images, text, actions, +/// navigation bar visibility, and title/subtitle views. @available(iOS 15, *) class LCENavigationState: ObservableObject { + /// The image view for the right button. @Published var rightButtonImage: AnyView? = nil + /// The text for the right button. @Published var rightButtonText: Text = Text("") + /// The action to perform when the right button is tapped. @Published var rightButtonAction: () -> Void = {} + /// The image view for the left button. @Published var leftButtonImage: AnyView? = nil + /// The text for the left button. @Published var leftButtonText: Text = Text("") + /// The action to perform when the left button is tapped. @Published var leftButtonAction: () -> Void = {} + /// A boolean value that controls the visibility of the navigation bar. @Published var hideNavigationBar: Bool = false + /// The title view of the navigation bar. @Published var title: (any View) = Text("") + /// The subtitle view of the navigation bar. @Published var subTitle: (any View) = Text("") } +/// `LCENavigationView` is a SwiftUI `View` that provides a customizable navigation bar. +/// It allows setting left and right buttons, a title, and a subtitle. @available(iOS 15, *) public struct LCENavigationView: 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: 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: 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: 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: View { } } + /// The private `NavLeftButton` view. private var NavLeftButton: some View { Button(action: state.leftButtonAction) { HStack { @@ -101,6 +126,7 @@ public struct LCENavigationView: View { } } + /// The private `NavRightButton` view. private var NavRightButton: some View { Button(action: state.rightButtonAction) { HStack { @@ -112,6 +138,12 @@ public struct LCENavigationView: 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: 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: 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: 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 {