diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 195abca..55bb603 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -1,9 +1,8 @@ name: Tests on: - push: - branches-ignore: - - master + pull_request: + types: [opened, synchronize] jobs: run_tests: @@ -12,4 +11,4 @@ jobs: - name: Prepare branch on runner uses: actions/checkout@v3 - name: Build & Test - run: xcodebuild build clean test -scheme "Uptech-iOS-Helpers" -destination "platform=iOS Simulator,name=IPhone 11" + run: xcodebuild build clean test -scheme "Uptech-iOS-Helpers-Package" -destination "platform=iOS Simulator,name=IPhone 15" diff --git a/Package.swift b/Package.swift index 4213c72..f0a2f7b 100644 --- a/Package.swift +++ b/Package.swift @@ -3,13 +3,23 @@ import PackageDescription let package = Package( - name: "Uptech-iOS-Helpers", - platforms: [.iOS(.v11)], - products: [ - .library(name: "Uptech-iOS-Helpers", targets: ["Uptech-iOS-Helpers"]), - ], - targets: [ - .target(name: "Uptech-iOS-Helpers", dependencies: []), - .testTarget(name: "Uptech-iOS-Helpers-Tests", dependencies: ["Uptech-iOS-Helpers"]), - ] + name: "Uptech-iOS-Helpers", + platforms: [.iOS(.v13)], + products: [ + .library(name: "Uptech-iOS-Helpers", targets: ["UptechFoundationHelper", "UptechUIKitHelper", "UptechSwiftUIHelper"]), + .library(name: "UptechFoundationHelper", targets: ["UptechFoundationHelper"]), + .library(name: "UptechUIKitHelper", targets: ["UptechUIKitHelper"]), + .library(name: "UptechSwiftUIHelper", targets: ["UptechSwiftUIHelper"]) + ], + targets: [ + .target(name: "UptechFoundationHelper", path: "Sources/Uptech-iOS-Helpers/FoundationHelper"), + .target(name: "UptechUIKitHelper", path: "Sources/Uptech-iOS-Helpers/UIKitHelper"), + .target(name: "UptechSwiftUIHelper", path: "Sources/Uptech-iOS-Helpers/SwiftUIHelper"), + .testTarget(name: "FoundationHelper-Test", + dependencies: ["UptechFoundationHelper"], + path: "Tests/Uptech-iOS-Helpers-Tests/FoundationHelper"), + .testTarget(name: "UIKitHelper-Test", + dependencies: ["UptechUIKitHelper"], + path: "Tests/Uptech-iOS-Helpers-Tests/UIKitHelper") + ] ) diff --git a/README.md b/README.md index b84d0fe..bfb900b 100644 --- a/README.md +++ b/README.md @@ -5,41 +5,79 @@ iOS helper library that contains commonly used code in **Uptech** iOS projects. ## What's included -- Protocols - - **ReusableCell** - - Protocol that helps with reusable cells. +
+ + 🔧 Foundation Helpers + + + - Array extensions + - safe subscript + - next, previous, remove *Element* methods + + - Collection: + - subscript by indexPath + +
+ +
+  + 🖼️ UIKit Helpers + + + - Protocols + - **ReusableCell** + + Protocol that helps with reusable cells. + + If used on UITableViewCell subclasses provides handy methods for registering and dequeueing cells in UITableView's - If used on UITableViewCell subclasses provides handy methods for registering and dequeueing cells in UITableView's - - - **NibInitializable** - - Protocol that helps to initialize view with xib. -- Table & Collection View helpers - - *dequeue* and *register* methods for cells that confirm's to **ReusableCell** and optionally **NibInitializable** (for cell's created via xibs) -- UI-in-code helpers - - UIView extensions: - - subview adding, insertions *with* constrains - - corner radius - - NSLayoutConstraint: - - priority changing method - - constraint activation method for array of constrains - - UILayoutPriority: - - frequently used values -- Array extensions - - safe subscript - - next, previous, remove *Element* methods -- Collection: - - subscript by indexPath + - **NibInitializable** + + Protocol that helps to initialize view with xib. + - Table & Collection View helpers + - *dequeue* and *register* methods for cells that confirm's to **ReusableCell** and optionally **NibInitializable** (for cell's created via xibs) + - UI-in-code helpers + - UIView extensions: + - subview adding, insertions *with* constrains + - corner radius + - NSLayoutConstraint: + - priority changing method + - constraint activation method for array of constrains + - UILayoutPriority: + - frequently used values + +
+ +
+ + 🕊️ SwiftUI helpers + + + - Layouts + - ProportionalHStack and ProportionalVStack + - Layouts that resizes views with given proportions + + - View extensions + - conditionalModifiers (if, if/else), regular modifier + - customOnChange + - size/frame/offset/safeAreaInsets readers + +
## Installation #### Swift Package Manager *Note: Instructions below are for using SwiftPM without the Xcode UI. It's the easiest to go to your Project Settings -> Swift Packages and add Package from there using link https://github.com/uptechteam/Uptech-iOS-Helpers.git* +Package contains 4 libraries: +- UptechFoundationHelper +- UptechUIKitHelper +- UptechSwiftUIHelper +- Uptech-iOS-Helpers (first 3 combined) + To integrate using Apple's Swift package manager, without Xcode integration, add the following as a dependency to your Package.swift: ```swift -.package(url: "https://github.com/uptechteam/Uptech-iOS-Helpers.git", .upToNextMajor(from: "1.0.0")) +.package(url: "https://github.com/uptechteam/Uptech-iOS-Helpers.git", .upToNextMajor(from: "2.0.0")) ``` #### CocoaPods diff --git a/Sources/Uptech-iOS-Helpers/Extensions/Foundation/Array+extensions.swift b/Sources/Uptech-iOS-Helpers/Extensions/Foundation/Array+extensions.swift deleted file mode 100644 index 27155d0..0000000 --- a/Sources/Uptech-iOS-Helpers/Extensions/Foundation/Array+extensions.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// Array+extensions.swift -// -// -// Created by Sergey Kletsov on 25.03.2022. -// - -import Foundation - -public extension Array { - /// Returns the element at the specified index if it is within bounds, otherwise nil. - subscript(safe index: Index) -> Element? { - indices.contains(index) ? self[index] : nil - } - - /// Returns the elements at the specified range if it is within bounds, otherwise empty array. - subscript(safe range: Range) -> [Element] { - guard - range.startIndex >= startIndex, - range.endIndex <= endIndex else { - return [] - } - - return Array(self[range]) - } - - /// Returns the elements at the specified closed range if it is within bounds, otherwise empty array. - subscript(safe range: ClosedRange) -> [Element] { - guard - range.lowerBound >= 0, - endIndex > 0, - range.upperBound <= endIndex else { - return [] - } - - let maxIndex = endIndex - 1 - let upperBound = Swift.min(range.upperBound, maxIndex) - return Array(self[range.lowerBound...upperBound]) - } -} - -public extension Array where Element: Equatable { - /// Removes first element in array. - /// - Parameter item: An element to remove. - mutating func remove(_ item: Element) { - guard let index = firstIndex(of: item) else { return } - remove(at: index) - } - - /// Returns next element in array after a given one. - /// - Parameter item: An element to search next element. - /// - Returns: Element after passed item. If passed item is last element in array will return nil. - func next(item: Element) -> Element? { - if let index = self.firstIndex(of: item) { - return self[safe: index + 1] - } - return nil - } - - /// Returns previous element in array before a given one. - /// - Parameter item: An element to search previous element. - /// - Returns: Element before passed item. If passed item is first element in array will return nil. - func previous(item: Element) -> Element? { - if let index = self.firstIndex(of: item) { - return self[safe: index - 1] - } - return nil - } -} diff --git a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/CACornerMask+extensions.swift b/Sources/Uptech-iOS-Helpers/Extensions/UIKit/CACornerMask+extensions.swift deleted file mode 100644 index 9d8f9ac..0000000 --- a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/CACornerMask+extensions.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// CACornerMask+extensions.swift -// -// -// Created by Sergey Kletsov on 25.03.2022. -// - -import UIKit - -public extension CACornerMask { - /// Contains all corner masks (MaxXMaxY, MinXMaxY, MaxXMinY, MinXMinY). - static let all: CACornerMask = [ - CACornerMask.layerMaxXMaxYCorner, - CACornerMask.layerMinXMaxYCorner, - CACornerMask.layerMaxXMinYCorner, - CACornerMask.layerMinXMinYCorner - ] -} diff --git a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/Collection+indexPath.swift b/Sources/Uptech-iOS-Helpers/Extensions/UIKit/Collection+indexPath.swift deleted file mode 100644 index 9e8ce69..0000000 --- a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/Collection+indexPath.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// Collection+indexPath.swift -// -// -// Created by Sergey Kletsov on 25.03.2022. -// - -import UIKit - -public extension Collection where Index == Int, Iterator.Element: Collection, Iterator.Element.Index == Int { - /// Returns Element in two dimensional array by IndexPath. - /// - Parameter indexPath: An IndexPath to get Element by section and row. - /// - Returns: Element by given indexPath. - subscript(indexPath: IndexPath) -> Iterator.Element.Iterator.Element { - self[indexPath.section][indexPath.row] - } -} diff --git a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/NSLayoutConstrain+extensions.swift b/Sources/Uptech-iOS-Helpers/Extensions/UIKit/NSLayoutConstrain+extensions.swift deleted file mode 100644 index acc84b6..0000000 --- a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/NSLayoutConstrain+extensions.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// NSLayoutConstrain+extensions.swift -// -// -// Created by Sergey Kletsov on 25.03.2022. -// - -import UIKit - -public extension NSLayoutConstraint { - @discardableResult - /// Returns self with given priority. - /// - Parameter priority: UILayoutPriority to set. - /// - Returns: self. - func withPriority(_ priority: UILayoutPriority) -> NSLayoutConstraint { - self.priority = priority - return self - } -} - -public extension Array where Element == NSLayoutConstraint { - /// Activates each constraint in self - func activate() { - NSLayoutConstraint.activate(self) - } -} diff --git a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/UICollectionView+extensions.swift b/Sources/Uptech-iOS-Helpers/Extensions/UIKit/UICollectionView+extensions.swift deleted file mode 100644 index f608129..0000000 --- a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/UICollectionView+extensions.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// UICollectionView+extensions.swift -// -// -// Created by Sergey Kletsov on 05.04.2022. -// - -import UIKit - -public extension UICollectionView { - /// Dequeue's reusable cell for index path with given cell type. - /// - Important: Will crash if cell type for given reuseIdentifier don't match passed cellType! - /// - Parameters: - /// - indexPath: The index path specifying the location of the cell. - /// - cellType: Type of cell that should be dequeued. Must be ReusableCell. - /// - Returns: UICollectionViewCell dequeued and cased to cellType. - func dequeueReusableCell(for indexPath: IndexPath, cellType: T.Type = T.self) -> T where T: ReusableCell { - guard let cell = self.dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else { - fatalError("Failed to dequeue a cell with identifier \(cellType.reuseIdentifier) matching type \(cellType.self).") - } - - return cell - } - - /// Registers a class to use in creating new table cells by cellType.reuseIdentifier. - /// - Parameter cellType: Cell type that will be registered by cellType.reuseIdentifier - func register(_ cellType: T.Type) where T: ReusableCell { - self.register(cellType.self, forCellWithReuseIdentifier: cellType.reuseIdentifier) - } - - /// Registers a nib object that contains a cell with the table view under a specified identifier by cellType.reuseIdentifier. - /// - Parameter cellType: Cell type, nib of witch will be registered by cellType.reuseIdentifier. - func register(_ cellType: T.Type) where T: ReusableCell & NibInitializable { - self.register(cellType.nib, forCellWithReuseIdentifier: cellType.reuseIdentifier) - } -} diff --git a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/UILayoutPriority+extensions.swift b/Sources/Uptech-iOS-Helpers/Extensions/UIKit/UILayoutPriority+extensions.swift deleted file mode 100644 index 0df979d..0000000 --- a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/UILayoutPriority+extensions.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// UILayoutPriority+extensions.swift -// -// -// Created by Sergey Kletsov on 25.03.2022. -// - -import UIKit - -public extension UILayoutPriority { - /// rawValue 999 - static let almostRequired = UILayoutPriority(999) - /// rawValue 751 - static let prioritizedCompressionResistance = UILayoutPriority(751) - /// rawValue 251 - static let prioritizedHugging = UILayoutPriority(251) -} diff --git a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/UITableView+extensions.swift b/Sources/Uptech-iOS-Helpers/Extensions/UIKit/UITableView+extensions.swift deleted file mode 100644 index 2349137..0000000 --- a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/UITableView+extensions.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// UITableView+extensions.swift -// -// -// Created by Sergey Kletsov on 05.04.2022. -// - -import UIKit - -public extension UITableView { - /// Dequeue's reusable cell for index path with given cell type. - /// - Important: Will crash if cell type for given reuseIdentifier don't match passed cellType! - /// - Parameters: - /// - indexPath: The index path specifying the location of the cell. - /// - cellType: Type of cell that should be dequeued. Must be ReusableCell. - /// - Returns: UITableViewCell dequeued and cased to cellType. - func dequeueReusableCell(for indexPath: IndexPath, cellType: T.Type = T.self) -> T where T: ReusableCell { - guard let cell = self.dequeueReusableCell(withIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else { - fatalError("Failed to dequeue a cell with identifier \(cellType.reuseIdentifier) matching type \(cellType.self).") - } - - return cell - } - - /// Registers a class to use in creating new table cells by cellType.reuseIdentifier. - /// - Parameter cellType: Cell type that will be registered by cellType.reuseIdentifier - func register(_ cellType: T.Type) where T: ReusableCell { - self.register(cellType.self, forCellReuseIdentifier: cellType.reuseIdentifier) - } - - /// Registers a nib object that contains a cell with the table view under a specified identifier by cellType.reuseIdentifier. - /// - Parameter cellType: Cell type, nib of witch will be registered by cellType.reuseIdentifier. - func register(_ cellType: T.Type) where T: ReusableCell & NibInitializable { - self.register(cellType.nib, forCellReuseIdentifier: cellType.reuseIdentifier) - } -} diff --git a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/UIView+extensions.swift b/Sources/Uptech-iOS-Helpers/Extensions/UIKit/UIView+extensions.swift deleted file mode 100644 index 89c37a3..0000000 --- a/Sources/Uptech-iOS-Helpers/Extensions/UIKit/UIView+extensions.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// UIView+extensions.swift -// -// -// Created by Sergey Kletsov on 25.03.2022. -// -import UIKit - -public extension UIView { - /// Adds a view as subview the self. - /// Set's view translatesAutoresizingMaskIntoConstraints to false. - /// Activates given constrains array. - /// - Parameters: - /// - view: View to add as subview and disable Autoresizing Mask. - /// - constraints: Constrains array to activate after adding subview and disabling Autoresizing Mask. - func addSubview( _ view: UIView, withConstraints constraints: [NSLayoutConstraint]) { - addSubview(view) - view.translatesAutoresizingMaskIntoConstraints = false - constraints.activate() - } - - /// Adds a view as subview the self. - /// Adds constrains that fill given view to self's bounds. - /// - Also will set given view's translatesAutoresizingMaskIntoConstraints to false. - /// - Parameter view: View to add as subview and fill to self's bounds. - func addSubviewWithEdgeConstraints(_ view: UIView) { - addSubview(view, withConstraints: [ - view.topAnchor.constraint(equalTo: topAnchor), - view.leadingAnchor.constraint(equalTo: leadingAnchor), - view.trailingAnchor.constraint(equalTo: trailingAnchor), - view.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } - - /// Inserts a view to self at given index with given constrains. - /// - Also will set given view's translatesAutoresizingMaskIntoConstraints to false. - /// - Parameters: - /// - view: View to insert as subview and disable Autoresizing Mask. - /// - index: The index in the array of the subviews property at which to insert the view. Default value .zero - /// - constraints: Constrains array to activate after adding subview and disabling Autoresizing Mask. - func insertSubview(_ view: UIView, at index: Int = .zero, withConstraints constraints: [NSLayoutConstraint] ) { - insertSubview(view, at: index) - view.translatesAutoresizingMaskIntoConstraints = false - constraints.activate() - } - - /// Inserts a view as subview the self. - /// Adds constrains that fill given view to self's bounds. - /// - Also will set given view's translatesAutoresizingMaskIntoConstraints to false. - /// - Parameters: - /// - view: View to insert as subview and fill to self's bounds. - /// - index: The index in the array of the subviews property at which to insert the view. Default value .zero - func insertSubviewWithEdgeConstraints(_ view: UIView, at index: Int = .zero) { - insertSubview(view, at: index, withConstraints: [ - view.topAnchor.constraint(equalTo: topAnchor), - view.leadingAnchor.constraint(equalTo: leadingAnchor), - view.trailingAnchor.constraint(equalTo: trailingAnchor), - view.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } - - /// Adds corner radius to given corners. - /// - Parameters: - /// - radius: Corner radius to add. - /// - corners: Corners to add corner radius to. Default value .all - func roundCornersContinuously(radius: CGFloat, corners: CACornerMask = .all) { - layer.maskedCorners = corners - layer.cornerRadius = radius - - if #available(iOS 13.0, *) { - layer.cornerCurve = .continuous - } - } -} diff --git a/Sources/Uptech-iOS-Helpers/FoundationHelper/Array+extensions.swift b/Sources/Uptech-iOS-Helpers/FoundationHelper/Array+extensions.swift new file mode 100644 index 0000000..ebd93af --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/FoundationHelper/Array+extensions.swift @@ -0,0 +1,67 @@ +// +// Array+extensions.swift +// +// +// Created by Sergey Kletsov on 25.03.2022. +// + +public extension Array { + /// Returns the element at the specified index if it is within bounds, otherwise nil. + subscript(safe index: Index) -> Element? { + indices.contains(index) ? self[index] : nil + } + + /// Returns the elements at the specified range if it is within bounds, otherwise empty array. + subscript(safe range: Range) -> [Element] { + guard + range.startIndex >= startIndex, + range.endIndex <= endIndex else { + return [] + } + + return Array(self[range]) + } + + /// Returns the elements at the specified closed range if it is within bounds, otherwise empty array. + subscript(safe range: ClosedRange) -> [Element] { + guard + range.lowerBound >= 0, + endIndex > 0, + range.upperBound <= endIndex else { + return [] + } + + let maxIndex = endIndex - 1 + let upperBound = Swift.min(range.upperBound, maxIndex) + return Array(self[range.lowerBound...upperBound]) + } +} + +public extension Array where Element: Equatable { + /// Removes first element in array. + /// - Parameter item: An element to remove. + mutating func remove(_ item: Element) { + guard let index = firstIndex(of: item) else { return } + remove(at: index) + } + + /// Returns next element in array after a given one. + /// - Parameter item: An element to search next element. + /// - Returns: Element after passed item. If passed item is last element in array will return nil. + func next(item: Element) -> Element? { + if let index = self.firstIndex(of: item) { + return self[safe: index + 1] + } + return nil + } + + /// Returns previous element in array before a given one. + /// - Parameter item: An element to search previous element. + /// - Returns: Element before passed item. If passed item is first element in array will return nil. + func previous(item: Element) -> Element? { + if let index = self.firstIndex(of: item) { + return self[safe: index - 1] + } + return nil + } +} diff --git a/Sources/Uptech-iOS-Helpers/Protocols/NibInitializable.swift b/Sources/Uptech-iOS-Helpers/Protocols/NibInitializable.swift deleted file mode 100644 index bef868c..0000000 --- a/Sources/Uptech-iOS-Helpers/Protocols/NibInitializable.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// NibInitializable.swift -// -// -// Created by Sergey Kletsov on 28.03.2022. -// - -import Foundation -import UIKit - -/// Protocol that helps to initialize view with xib. -/// All properties and method have default implementation. -public protocol NibInitializable { - /// Default implementation returns class name. - static var nibName: String { get } - /// Default implementation returns nib with self.nibName in default bundle. - static var nib: UINib { get } - /// Returns view initiated from xib - /// - Returns: Default implementation returns first view in self.nib casted to Self. - static func initFromNib() -> Self -} - -public extension NibInitializable where Self: UIView { - static var nibName: String { - String(describing: Self.self) - } - - static var nib: UINib { - UINib(nibName: nibName, bundle: nil) - } - - static func initFromNib() -> Self { - guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else { - fatalError("Could not instantiate view from nib with name \(nibName).") - } - - return view - } -} diff --git a/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Extensions/View+GeometryReader.swift b/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Extensions/View+GeometryReader.swift new file mode 100644 index 0000000..921f9f4 --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Extensions/View+GeometryReader.swift @@ -0,0 +1,79 @@ +// +// View+GeometryReader.swift +// +// +// Created by Sergey Kletsov on 02.08.2024. +// + +import SwiftUI + +private struct SizePreferenceKey: PreferenceKey { + static var defaultValue: CGSize = .zero + + static func reduce(value: inout CGSize, nextValue: () -> CGSize) {} +} + +private struct GlobalFramePreferenceKey: PreferenceKey { + static var defaultValue: CGRect = .zero + + static func reduce(value: inout CGRect, nextValue: () -> CGRect) {} +} + +public extension View { + /// Modifier to read view size. Uses GeometryReader + /// - Parameter onChange: A closure to run when size changes + /// - Returns: A view that calls onChange closure when the view size changes. + func readSize(onChange: @escaping (CGSize) -> Void) -> some View { + background( + GeometryReader { geometryProxy in + Color.clear + .preference(key: SizePreferenceKey.self, value: geometryProxy.size) + } + ) + .onPreferenceChange(SizePreferenceKey.self, perform: onChange) + } + + /// Modifier to read view frame. Uses GeometryReader + /// - Parameters: + /// - coordinateSpace: Coordinate space to get frame + /// - onChange: A closure to run when frame changes + /// - Returns: A view that calls onChange closure when the view frame changes in given coordinate space. + func readFrame(coordinateSpace: CoordinateSpace, onChange: @escaping (CGRect) -> Void) -> some View { + background( + GeometryReader { geometryProxy in + Color.clear + .preference(key: GlobalFramePreferenceKey.self, value: geometryProxy.frame(in: coordinateSpace)) + } + ) + .onPreferenceChange(GlobalFramePreferenceKey.self, perform: onChange) + } + + /// Modifier to read view frame in global coordinate space. Uses GeometryReader + /// - onChange: A closure to run when frame changes + /// - Returns: A view that calls onChange closure when the view frame changes in global coordinate space. + func readGlobalFrame(onChange: @escaping (CGRect) -> Void) -> some View { + readFrame(coordinateSpace: .global, onChange: onChange) + } + + /// Modifier to read view origin in global coordinate space. Uses GeometryReader + /// - onChange: A closure to run when frame's origin changes + /// - Returns: A view that calls onChange closure when the view frame's origin changes in global coordinate space. + func readOffset(onChange: @escaping (CGPoint) -> Void) -> some View { + readFrame(coordinateSpace: .global) { frame in + onChange(frame.origin) + } + } + + /// Modifier to read view safeAreaInsets. Embeds view inside GeometryReader + /// - Parameter closure: A closure to run when view safeAreaInsets are configured (after view appeared) + /// - Returns: GeometryReader that contains view and calls closure after view is appeared with calculated safeAreaInsets + func readInsets(_ closure: @escaping (EdgeInsets) -> Void) -> some View { + GeometryReader { geometryProxy in + self.onAppear { + DispatchQueue.main.async { + closure(geometryProxy.safeAreaInsets) + } + } + } + } +} diff --git a/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Extensions/View+conditionalModifier.swift b/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Extensions/View+conditionalModifier.swift new file mode 100644 index 0000000..60914b7 --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Extensions/View+conditionalModifier.swift @@ -0,0 +1,65 @@ +// +// View+conditionalModifier.swift +// +// +// Created by Sergey Kletsov on 02.08.2024. +// + +import SwiftUI + +public extension View { + typealias ContentTransform = (Self) -> Content + + /// Conditional Modifier method that modifies view depending of input Bool + /// - Parameters: + /// - condition: Bool condition to check + /// - ifTrue: Modifier closure that will be applied if condition is true + /// - ifFalse: Modifier closure that will be applied if condition is false + /// - Returns: Self modified by ifTrue or ifFalse closure + @ViewBuilder + func conditionalModifier( + _ condition: @autoclosure () -> Bool, + ifTrue: ContentTransform, + else ifFalse: ContentTransform + ) -> some View { + if condition() { + ifTrue(self) + } else { + ifFalse(self) + } + } + + /// Conditional Modifier method that modifies view depending of input Bool + /// - Parameters: + /// - condition: Bool condition to check + /// - ifTrue: Modifier closure that will be applied if condition is true + /// - ifFalse: Modifier closure that will be applied if condition is false + /// - Returns: Self modified by ifTrue or ifFalse closure + + /// Conditional Modifier method that modifies view depending of input Bool + /// - Parameters: + /// - condition: Bool condition to check + /// - transform: Modifier closure that will be applied if condition is true + /// - Returns: Self modified by ifTrue closure if condition is true, Self otherwise + @ViewBuilder + func `if`( + _ condition: @autoclosure () -> Bool, + transform: ContentTransform + ) -> some View { + conditionalModifier( + condition(), + ifTrue: transform, + else: { $0 } + ) + } + + /// Modifier method that modifies view + /// - Parameter transform: Modifier closure to apply + /// - Returns: Self modified by transform closure + @ViewBuilder + func modify( + @ViewBuilder transform: ContentTransform + ) -> some View { + transform(self) + } +} diff --git a/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Extensions/View+customOnChange.swift b/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Extensions/View+customOnChange.swift new file mode 100644 index 0000000..1f94722 --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Extensions/View+customOnChange.swift @@ -0,0 +1,31 @@ +// +// View+customOnChange.swift +// +// +// Created by Sergey Kletsov on 02.08.2024. +// + +import SwiftUI + +public extension View { + /// Adds a modifier for this view that fires an action when a specific value changes. + /// Calls different methods depending of iOS version (new system onChange modifier was introduced in iOS 17) + /// - Parameters: + /// - value: The value to check against when determining whether to run the closure. + /// - initial: Whether the action should be run when this view initially appears. (will be used only on iOS 17) + /// - action: A closure to run when the value changes. Will use newValue on iOS 17 + /// - Returns: A view that fires an action when the specified value changes. + @available(iOS 14.0, *) + @ViewBuilder + func customOnChange( + of value: V, + initial: Bool = false, + _ action: @escaping (V) -> Void + ) -> some View where V: Equatable { + if #available(iOS 17.0, *) { + self.onChange(of: value, initial: initial, { _, newValue in action(newValue) }) + } else { + self.onChange(of: value, perform: action) + } + } +} diff --git a/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Layouts/ProportionalHStack.swift b/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Layouts/ProportionalHStack.swift new file mode 100644 index 0000000..033b9ea --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Layouts/ProportionalHStack.swift @@ -0,0 +1,56 @@ +// +// ProportionalHStack.swift +// +// +// Created by Sergey Kletsov on 07.08.2024. +// + +import SwiftUI + +/// HStack layout that layouts subviews by given proportions +@available(iOS 16.0, *) +struct ProportionalHStack: Layout { + let proportions: [CGFloat] + let spacing: CGFloat + + /// Creates new layout with given proportions and spacing + /// - Parameters: + /// - proportions: Array of proportions. Numbers in array should add up to **1.0**. Number of proportions should be equal to number of subviews. + /// - spacing: Spacing between subviews. Default value is 0 + init(proportions: [CGFloat], spacing: CGFloat = 0) { + self.proportions = proportions + self.spacing = spacing + } + + func sizeThatFits( + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout Void + ) -> CGSize { + proposal.replacingUnspecifiedDimensions() + } + + func placeSubviews( + in bounds: CGRect, + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout Void + ) { + guard subviews.count == proportions.count else { + return + } + + let totalWidth = bounds.width - (spacing * CGFloat(subviews.count - 1)) + var xOffset: CGFloat = bounds.minX + + for (index, subview) in subviews.enumerated() { + let width = totalWidth * proportions[index] + subview.place( + at: CGPoint(x: xOffset, y: bounds.midY), + anchor: .leading, + proposal: ProposedViewSize(width: width, height: bounds.height) + ) + xOffset += width + spacing + } + } +} diff --git a/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Layouts/ProportionalVStack.swift b/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Layouts/ProportionalVStack.swift new file mode 100644 index 0000000..de9a991 --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/SwiftUIHelper/Layouts/ProportionalVStack.swift @@ -0,0 +1,56 @@ +// +// ProportionalVStack.swift +// +// +// Created by Sergey Kletsov on 07.08.2024. +// + +import SwiftUI + +/// VStack layout that layouts subviews by given proportions +@available(iOS 16.0, *) +public struct ProportionalVStack: Layout { + private let proportions: [CGFloat] + private let spacing: CGFloat + + /// Creates new layout with given proportions and spacing + /// - Parameters: + /// - proportions: Array of proportions. Numbers in array should add up to **1.0**. Number of proportions should be equal to number of subviews. + /// - spacing: Spacing between subviews. Default value is 0 + init(proportions: [CGFloat], spacing: CGFloat = 0) { + self.proportions = proportions + self.spacing = spacing + } + + public func sizeThatFits( + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout Void + ) -> CGSize { + proposal.replacingUnspecifiedDimensions() + } + + public func placeSubviews( + in bounds: CGRect, + proposal: ProposedViewSize, + subviews: Subviews, + cache: inout Void + ) { + guard subviews.count == proportions.count else { + return + } + + let totalHeight = bounds.height - (spacing * CGFloat(subviews.count - 1)) + var yOffset: CGFloat = bounds.minY + + for (index, subview) in subviews.enumerated() { + let height = totalHeight * proportions[index] + subview.place( + at: CGPoint(x: bounds.midX, y: yOffset), + anchor: .top, + proposal: ProposedViewSize(width: bounds.width, height: height) + ) + yOffset += height + spacing + } + } +} diff --git a/Sources/Uptech-iOS-Helpers/UIKitHelper/CACornerMask+extensions.swift b/Sources/Uptech-iOS-Helpers/UIKitHelper/CACornerMask+extensions.swift new file mode 100644 index 0000000..70075b3 --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/UIKitHelper/CACornerMask+extensions.swift @@ -0,0 +1,18 @@ +// +// CACornerMask+extensions.swift +// +// +// Created by Sergey Kletsov on 25.03.2022. +// + +import UIKit + +public extension CACornerMask { + /// Contains all corner masks (MaxXMaxY, MinXMaxY, MaxXMinY, MinXMinY). + static let all: CACornerMask = [ + CACornerMask.layerMaxXMaxYCorner, + CACornerMask.layerMinXMaxYCorner, + CACornerMask.layerMaxXMinYCorner, + CACornerMask.layerMinXMinYCorner + ] +} diff --git a/Sources/Uptech-iOS-Helpers/UIKitHelper/Collection+indexPath.swift b/Sources/Uptech-iOS-Helpers/UIKitHelper/Collection+indexPath.swift new file mode 100644 index 0000000..f3c83a0 --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/UIKitHelper/Collection+indexPath.swift @@ -0,0 +1,17 @@ +// +// Collection+indexPath.swift +// +// +// Created by Sergey Kletsov on 25.03.2022. +// + +import UIKit + +public extension Collection where Index == Int, Iterator.Element: Collection, Iterator.Element.Index == Int { + /// Returns Element in two dimensional array by IndexPath. + /// - Parameter indexPath: An IndexPath to get Element by section and row. + /// - Returns: Element by given indexPath. + subscript(indexPath: IndexPath) -> Iterator.Element.Iterator.Element { + self[indexPath.section][indexPath.row] + } +} diff --git a/Sources/Uptech-iOS-Helpers/UIKitHelper/NSLayoutConstrain+extensions.swift b/Sources/Uptech-iOS-Helpers/UIKitHelper/NSLayoutConstrain+extensions.swift new file mode 100644 index 0000000..2d6a459 --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/UIKitHelper/NSLayoutConstrain+extensions.swift @@ -0,0 +1,26 @@ +// +// NSLayoutConstrain+extensions.swift +// +// +// Created by Sergey Kletsov on 25.03.2022. +// + +import UIKit + +public extension NSLayoutConstraint { + @discardableResult + /// Returns self with given priority. + /// - Parameter priority: UILayoutPriority to set. + /// - Returns: self. + func withPriority(_ priority: UILayoutPriority) -> NSLayoutConstraint { + self.priority = priority + return self + } +} + +public extension Array where Element == NSLayoutConstraint { + /// Activates each constraint in self + func activate() { + NSLayoutConstraint.activate(self) + } +} diff --git a/Sources/Uptech-iOS-Helpers/UIKitHelper/Protocols/NibInitializable.swift b/Sources/Uptech-iOS-Helpers/UIKitHelper/Protocols/NibInitializable.swift new file mode 100644 index 0000000..a9ee774 --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/UIKitHelper/Protocols/NibInitializable.swift @@ -0,0 +1,38 @@ +// +// NibInitializable.swift +// +// +// Created by Sergey Kletsov on 28.03.2022. +// + +import UIKit + +/// Protocol that helps to initialize view with xib. +/// All properties and method have default implementation. +public protocol NibInitializable { + /// Default implementation returns class name. + static var nibName: String { get } + /// Default implementation returns nib with self.nibName in default bundle. + static var nib: UINib { get } + /// Returns view initiated from xib + /// - Returns: Default implementation returns first view in self.nib casted to Self. + static func initFromNib() -> Self +} + +public extension NibInitializable where Self: UIView { + static var nibName: String { + String(describing: Self.self) + } + + static var nib: UINib { + UINib(nibName: nibName, bundle: nil) + } + + static func initFromNib() -> Self { + guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else { + fatalError("Could not instantiate view from nib with name \(nibName).") + } + + return view + } +} diff --git a/Sources/Uptech-iOS-Helpers/Protocols/ReusableCell.swift b/Sources/Uptech-iOS-Helpers/UIKitHelper/Protocols/ReusableCell.swift similarity index 66% rename from Sources/Uptech-iOS-Helpers/Protocols/ReusableCell.swift rename to Sources/Uptech-iOS-Helpers/UIKitHelper/Protocols/ReusableCell.swift index 0574479..eb32886 100644 --- a/Sources/Uptech-iOS-Helpers/Protocols/ReusableCell.swift +++ b/Sources/Uptech-iOS-Helpers/UIKitHelper/Protocols/ReusableCell.swift @@ -1,6 +1,6 @@ // // ReusableCell.swift -// +// // // Created by Sergey Kletsov on 28.03.2022. // @@ -10,12 +10,12 @@ import UIKit /// Protocol that helps with reusable cells. If used on UITableViewCell subclasses provides handy methods for registering and dequeueing cells in UITableView's /// Has default implementation for reuseIdentifier. public protocol ReusableCell { - /// Default implementation return's class name - static var reuseIdentifier: String { get } + /// Default implementation return's class name + static var reuseIdentifier: String { get } } public extension ReusableCell { - static var reuseIdentifier: String { - String(describing: Self.self) - } + static var reuseIdentifier: String { + String(describing: Self.self) + } } diff --git a/Sources/Uptech-iOS-Helpers/UIKitHelper/UICollectionView+extensions.swift b/Sources/Uptech-iOS-Helpers/UIKitHelper/UICollectionView+extensions.swift new file mode 100644 index 0000000..a406063 --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/UIKitHelper/UICollectionView+extensions.swift @@ -0,0 +1,36 @@ +// +// UICollectionView+extensions.swift +// +// +// Created by Sergey Kletsov on 05.04.2022. +// + +import UIKit + +public extension UICollectionView { + /// Dequeue's reusable cell for index path with given cell type. + /// - Important: Will crash if cell type for given reuseIdentifier don't match passed cellType! + /// - Parameters: + /// - indexPath: The index path specifying the location of the cell. + /// - cellType: Type of cell that should be dequeued. Must be ReusableCell. + /// - Returns: UICollectionViewCell dequeued and cased to cellType. + func dequeueReusableCell(for indexPath: IndexPath, cellType: T.Type = T.self) -> T where T: ReusableCell { + guard let cell = self.dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else { + fatalError("Failed to dequeue a cell with identifier \(cellType.reuseIdentifier) matching type \(cellType.self).") + } + + return cell + } + + /// Registers a class to use in creating new table cells by cellType.reuseIdentifier. + /// - Parameter cellType: Cell type that will be registered by cellType.reuseIdentifier + func register(_ cellType: T.Type) where T: ReusableCell { + self.register(cellType.self, forCellWithReuseIdentifier: cellType.reuseIdentifier) + } + + /// Registers a nib object that contains a cell with the table view under a specified identifier by cellType.reuseIdentifier. + /// - Parameter cellType: Cell type, nib of witch will be registered by cellType.reuseIdentifier. + func register(_ cellType: T.Type) where T: ReusableCell & NibInitializable { + self.register(cellType.nib, forCellWithReuseIdentifier: cellType.reuseIdentifier) + } +} diff --git a/Sources/Uptech-iOS-Helpers/UIKitHelper/UILayoutPriority+extensions.swift b/Sources/Uptech-iOS-Helpers/UIKitHelper/UILayoutPriority+extensions.swift new file mode 100644 index 0000000..e40ca7a --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/UIKitHelper/UILayoutPriority+extensions.swift @@ -0,0 +1,17 @@ +// +// UILayoutPriority+extensions.swift +// +// +// Created by Sergey Kletsov on 25.03.2022. +// + +import UIKit + +public extension UILayoutPriority { + /// rawValue 999 + static let almostRequired = UILayoutPriority(999) + /// rawValue 751 + static let prioritizedCompressionResistance = UILayoutPriority(751) + /// rawValue 251 + static let prioritizedHugging = UILayoutPriority(251) +} diff --git a/Sources/Uptech-iOS-Helpers/UIKitHelper/UITableView+extensions.swift b/Sources/Uptech-iOS-Helpers/UIKitHelper/UITableView+extensions.swift new file mode 100644 index 0000000..8d44675 --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/UIKitHelper/UITableView+extensions.swift @@ -0,0 +1,36 @@ +// +// UITableView+extensions.swift +// +// +// Created by Sergey Kletsov on 05.04.2022. +// + +import UIKit + +public extension UITableView { + /// Dequeue's reusable cell for index path with given cell type. + /// - Important: Will crash if cell type for given reuseIdentifier don't match passed cellType! + /// - Parameters: + /// - indexPath: The index path specifying the location of the cell. + /// - cellType: Type of cell that should be dequeued. Must be ReusableCell. + /// - Returns: UITableViewCell dequeued and cased to cellType. + func dequeueReusableCell(for indexPath: IndexPath, cellType: T.Type = T.self) -> T where T: ReusableCell { + guard let cell = self.dequeueReusableCell(withIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else { + fatalError("Failed to dequeue a cell with identifier \(cellType.reuseIdentifier) matching type \(cellType.self).") + } + + return cell + } + + /// Registers a class to use in creating new table cells by cellType.reuseIdentifier. + /// - Parameter cellType: Cell type that will be registered by cellType.reuseIdentifier + func register(_ cellType: T.Type) where T: ReusableCell { + self.register(cellType.self, forCellReuseIdentifier: cellType.reuseIdentifier) + } + + /// Registers a nib object that contains a cell with the table view under a specified identifier by cellType.reuseIdentifier. + /// - Parameter cellType: Cell type, nib of witch will be registered by cellType.reuseIdentifier. + func register(_ cellType: T.Type) where T: ReusableCell & NibInitializable { + self.register(cellType.nib, forCellReuseIdentifier: cellType.reuseIdentifier) + } +} diff --git a/Sources/Uptech-iOS-Helpers/UIKitHelper/UIView+extensions.swift b/Sources/Uptech-iOS-Helpers/UIKitHelper/UIView+extensions.swift new file mode 100644 index 0000000..c0cac35 --- /dev/null +++ b/Sources/Uptech-iOS-Helpers/UIKitHelper/UIView+extensions.swift @@ -0,0 +1,74 @@ +// +// UIView+extensions.swift +// +// +// Created by Sergey Kletsov on 25.03.2022. +// +import UIKit + +public extension UIView { + /// Adds a view as subview the self. + /// Set's view translatesAutoresizingMaskIntoConstraints to false. + /// Activates given constrains array. + /// - Parameters: + /// - view: View to add as subview and disable Autoresizing Mask. + /// - constraints: Constrains array to activate after adding subview and disabling Autoresizing Mask. + func addSubview( _ view: UIView, withConstraints constraints: [NSLayoutConstraint]) { + addSubview(view) + view.translatesAutoresizingMaskIntoConstraints = false + constraints.activate() + } + + /// Adds a view as subview the self. + /// Adds constrains that fill given view to self's bounds. + /// - Also will set given view's translatesAutoresizingMaskIntoConstraints to false. + /// - Parameter view: View to add as subview and fill to self's bounds. + func addSubviewWithEdgeConstraints(_ view: UIView) { + addSubview(view, withConstraints: [ + view.topAnchor.constraint(equalTo: topAnchor), + view.leadingAnchor.constraint(equalTo: leadingAnchor), + view.trailingAnchor.constraint(equalTo: trailingAnchor), + view.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } + + /// Inserts a view to self at given index with given constrains. + /// - Also will set given view's translatesAutoresizingMaskIntoConstraints to false. + /// - Parameters: + /// - view: View to insert as subview and disable Autoresizing Mask. + /// - index: The index in the array of the subviews property at which to insert the view. Default value .zero + /// - constraints: Constrains array to activate after adding subview and disabling Autoresizing Mask. + func insertSubview(_ view: UIView, at index: Int = .zero, withConstraints constraints: [NSLayoutConstraint] ) { + insertSubview(view, at: index) + view.translatesAutoresizingMaskIntoConstraints = false + constraints.activate() + } + + /// Inserts a view as subview the self. + /// Adds constrains that fill given view to self's bounds. + /// - Also will set given view's translatesAutoresizingMaskIntoConstraints to false. + /// - Parameters: + /// - view: View to insert as subview and fill to self's bounds. + /// - index: The index in the array of the subviews property at which to insert the view. Default value .zero + func insertSubviewWithEdgeConstraints(_ view: UIView, at index: Int = .zero) { + insertSubview(view, at: index, withConstraints: [ + view.topAnchor.constraint(equalTo: topAnchor), + view.leadingAnchor.constraint(equalTo: leadingAnchor), + view.trailingAnchor.constraint(equalTo: trailingAnchor), + view.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } + + /// Adds corner radius to given corners. + /// - Parameters: + /// - radius: Corner radius to add. + /// - corners: Corners to add corner radius to. Default value .all + func roundCornersContinuously(radius: CGFloat, corners: CACornerMask = .all) { + layer.maskedCorners = corners + layer.cornerRadius = radius + + if #available(iOS 13.0, *) { + layer.cornerCurve = .continuous + } + } +} diff --git a/Tests/Uptech-iOS-Helpers-Tests/Foundation/Array+extensionsTests.swift b/Tests/Uptech-iOS-Helpers-Tests/Foundation/Array+extensionsTests.swift deleted file mode 100644 index a3bd358..0000000 --- a/Tests/Uptech-iOS-Helpers-Tests/Foundation/Array+extensionsTests.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// Array+extensionsTests.swift -// -// -// Created by Sergey Kletsov on 25.03.2022. -// - -import XCTest -@testable import Uptech_iOS_Helpers - -final class ArrayExtensionsTests: XCTestCase { - private let sut = [0, 1, 2, 3, 4] - - func testSafeElementReturnsElement() throws { - let result = sut[safe: 2] - let expectedResult = 2 - XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") - } - - func testSafeElementReturnsNil() throws { - let result = sut[safe: 10] - XCTAssertNil(result, "Result should be nil") - } - - func testSafeRangeReturnsElements() throws { - let result = sut[safe: 1..<3] - let expectedResult = [1, 2] - XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") - } - - func testSafeNegativeRangeReturnsEmpty() throws { - let result = sut[safe: -3..<(-1)] - let expectedResult: [Int] = [] - XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") - } - - func testSafeRangeReturnsEmpty() throws { - let result = sut[safe: 7..<10] - let expectedResult: [Int] = [] - XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") - } - - func testSafeClosedRangeReturnsElements() throws { - let result = sut[safe: 1...3] - let expectedResult = [1, 2, 3] - XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") - } - - func testSafeNegativeClosedRangeReturnsEmpty() throws { - let result = sut[safe: -3...(-1)] - let expectedResult: [Int] = [] - XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") - } - - func testSafeClosedRangeReturnsEmpty() throws { - let result = sut[safe: 7...10] - let expectedResult: [Int] = [] - XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") - } - - func testRemoveExistingElement() throws { - var sut = self.sut - sut.remove(1) - let expectedResult = [0, 2, 3, 4] - XCTAssertEqual(sut, expectedResult, "Result should be \(expectedResult)") - } - - func testRemoveNonExistingElement() throws { - var sut = self.sut - sut.remove(5) - let expectedResult = self.sut - XCTAssertEqual(sut, expectedResult, "Result should be \(expectedResult)") - } - - func testNextElementExist() throws { - let result = sut.next(item: 2) - let expectedResult = 3 - XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") - } - - func testNextElementNil() throws { - let result = sut.next(item: 4) - XCTAssertNil(result, "Result should be nil") - } - - func testPreviousElementExist() throws { - let result = sut.previous(item: 2) - let expectedResult = 1 - XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") - } - - func testPreviousElementNil() throws { - let result = sut.previous(item: 0) - XCTAssertNil(result, "Result should be nil") - } -} diff --git a/Tests/Uptech-iOS-Helpers-Tests/FoundationHelper/Array+extensionsTests.swift b/Tests/Uptech-iOS-Helpers-Tests/FoundationHelper/Array+extensionsTests.swift new file mode 100644 index 0000000..642ed5e --- /dev/null +++ b/Tests/Uptech-iOS-Helpers-Tests/FoundationHelper/Array+extensionsTests.swift @@ -0,0 +1,96 @@ +// +// Array+extensionsTests.swift +// +// +// Created by Sergey Kletsov on 25.03.2022. +// + +import XCTest +@testable import UptechFoundationHelper + +final class ArrayExtensionsTests: XCTestCase { + private let sut = [0, 1, 2, 3, 4] + + func testSafeElementReturnsElement() throws { + let result = sut[safe: 2] + let expectedResult = 2 + XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") + } + + func testSafeElementReturnsNil() throws { + let result = sut[safe: 10] + XCTAssertNil(result, "Result should be nil") + } + + func testSafeRangeReturnsElements() throws { + let result = sut[safe: 1..<3] + let expectedResult = [1, 2] + XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") + } + + func testSafeNegativeRangeReturnsEmpty() throws { + let result = sut[safe: -3..<(-1)] + let expectedResult: [Int] = [] + XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") + } + + func testSafeRangeReturnsEmpty() throws { + let result = sut[safe: 7..<10] + let expectedResult: [Int] = [] + XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") + } + + func testSafeClosedRangeReturnsElements() throws { + let result = sut[safe: 1...3] + let expectedResult = [1, 2, 3] + XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") + } + + func testSafeNegativeClosedRangeReturnsEmpty() throws { + let result = sut[safe: -3...(-1)] + let expectedResult: [Int] = [] + XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") + } + + func testSafeClosedRangeReturnsEmpty() throws { + let result = sut[safe: 7...10] + let expectedResult: [Int] = [] + XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") + } + + func testRemoveExistingElement() throws { + var sut = self.sut + sut.remove(1) + let expectedResult = [0, 2, 3, 4] + XCTAssertEqual(sut, expectedResult, "Result should be \(expectedResult)") + } + + func testRemoveNonExistingElement() throws { + var sut = self.sut + sut.remove(5) + let expectedResult = self.sut + XCTAssertEqual(sut, expectedResult, "Result should be \(expectedResult)") + } + + func testNextElementExist() throws { + let result = sut.next(item: 2) + let expectedResult = 3 + XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") + } + + func testNextElementNil() throws { + let result = sut.next(item: 4) + XCTAssertNil(result, "Result should be nil") + } + + func testPreviousElementExist() throws { + let result = sut.previous(item: 2) + let expectedResult = 1 + XCTAssertEqual(result, expectedResult, "Result should be \(expectedResult)") + } + + func testPreviousElementNil() throws { + let result = sut.previous(item: 0) + XCTAssertNil(result, "Result should be nil") + } +} diff --git a/Tests/Uptech-iOS-Helpers-Tests/UIKit/Collection+indexPathTests.swift b/Tests/Uptech-iOS-Helpers-Tests/UIKit/Collection+indexPathTests.swift deleted file mode 100644 index 8ae87da..0000000 --- a/Tests/Uptech-iOS-Helpers-Tests/UIKit/Collection+indexPathTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// Collection+indexPathTests.swift -// -// -// Created by Sergey Kletsov on 29.03.2022. -// - -import XCTest -@testable import Uptech_iOS_Helpers - -final class CollectionIndexPathTests: XCTestCase { - private let sut = [[0, 1, 2], - [3, 4, 5], - [6, 7, 8]] - - func testIndexPathSubscriptFirstItem() { - let result = sut[IndexPath(item: 0, section: 0)] - let expectedResult = 0 - - XCTAssertEqual(result, expectedResult) - } - - func testIndexPathSubscriptMiddleItem() { - let result = sut[IndexPath(row: 1, section: 1)] - let expectedResult = 4 - - XCTAssertEqual(result, expectedResult) - } - - func testIndexPathSubscriptLastItem() { - let result = sut[IndexPath(item: 2, section: 2)] - let expectedResult = 8 - - XCTAssertEqual(result, expectedResult) - } - -} diff --git a/Tests/Uptech-iOS-Helpers-Tests/UIKit/NSLayoutConstraint+extensionsTests.swift b/Tests/Uptech-iOS-Helpers-Tests/UIKit/NSLayoutConstraint+extensionsTests.swift deleted file mode 100644 index 9117f22..0000000 --- a/Tests/Uptech-iOS-Helpers-Tests/UIKit/NSLayoutConstraint+extensionsTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// NSLayoutConstraint+extensionsTests.swift -// -// -// Created by Sergey Kletsov on 29.03.2022. -// - -import XCTest -@testable import Uptech_iOS_Helpers - -final class NSLayoutConstraintExtensionsTests: XCTestCase { - - func testWithPriority() throws { - let sut = NSLayoutConstraint() - let priority = UILayoutPriority.almostRequired - let result = sut.withPriority(priority) - XCTAssertEqual(result.priority, priority, "sut's priority should be \(priority)") - } - - func testActivateExtension() throws { - let rootView = UIView() - let subview = UIView() - - subview.translatesAutoresizingMaskIntoConstraints = false - rootView.addSubview(subview) - - let sut = [subview.heightAnchor.constraint(equalToConstant: 100), - subview.widthAnchor.constraint(equalTo: rootView.widthAnchor), - subview.centerYAnchor.constraint(equalTo: rootView.centerYAnchor), - subview.centerXAnchor.constraint(equalTo: rootView.centerXAnchor)] - - sut.activate() - XCTAssertTrue(sut.allSatisfy(\.isActive), "all constrains in sut array should be activated") - } - -} diff --git a/Tests/Uptech-iOS-Helpers-Tests/UIKit/UICollectionView+extensionsTests.swift b/Tests/Uptech-iOS-Helpers-Tests/UIKit/UICollectionView+extensionsTests.swift deleted file mode 100644 index 0448e72..0000000 --- a/Tests/Uptech-iOS-Helpers-Tests/UIKit/UICollectionView+extensionsTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// UICollectionView+extensionsTests.swift -// -// -// Created by Sergey Kletsov on 05.04.2022. -// - -import XCTest -import UIKit -@testable import Uptech_iOS_Helpers - -final class UICollectionViewExtensionsTests: XCTestCase { - private class TestCell: UICollectionViewCell, ReusableCell { } - private let cellType = TestCell.self - - func testRegisterCorrectType() throws { - let collectionView = createCollectionView() - collectionView.register(cellType) - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: .init(item: 0, section: 0)) - let isCellRightType = cell.isKind(of: cellType) - XCTAssertTrue(isCellRightType, "dequeued cell must be \(cellType)") - } - - func testDequeueNotCrashing() { - let collectionView = createCollectionView() - collectionView.register(cellType, forCellWithReuseIdentifier: cellType.reuseIdentifier) - let _ = collectionView.dequeueReusableCell(for: .init(item: 0, section: 0), cellType: cellType) - } - - private func createCollectionView() -> UICollectionView { - UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout()) - } -} diff --git a/Tests/Uptech-iOS-Helpers-Tests/UIKit/UITableView+extensionsTests.swift b/Tests/Uptech-iOS-Helpers-Tests/UIKit/UITableView+extensionsTests.swift deleted file mode 100644 index 0517f81..0000000 --- a/Tests/Uptech-iOS-Helpers-Tests/UIKit/UITableView+extensionsTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// UITableView+extensionsTests.swift -// -// -// Created by Sergey Kletsov on 05.04.2022. -// - -import XCTest -import UIKit -@testable import Uptech_iOS_Helpers - -final class UITableViewExtensionsTests: XCTestCase { - private class TestCell: UITableViewCell, ReusableCell { } - private let cellType = TestCell.self - - func testRegisterCorrectType() throws { - let tableView = UITableView() - tableView.register(cellType) - let cell = tableView.dequeueReusableCell(withIdentifier: cellType.reuseIdentifier, for: .init(item: 0, section: 0)) - let isCellRightType = cell.isKind(of: cellType) - XCTAssertTrue(isCellRightType, "dequeued cell must be \(cellType)") - } - - func testDequeueNotCrashing() { - let tableView = UITableView() - tableView.register(cellType, forCellReuseIdentifier: cellType.reuseIdentifier) - let _ = tableView.dequeueReusableCell(for: .init(item: 0, section: 0), cellType: cellType) - } -} diff --git a/Tests/Uptech-iOS-Helpers-Tests/UIKit/UIView+extensionsTests.swift b/Tests/Uptech-iOS-Helpers-Tests/UIKit/UIView+extensionsTests.swift deleted file mode 100644 index 38de2a5..0000000 --- a/Tests/Uptech-iOS-Helpers-Tests/UIKit/UIView+extensionsTests.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// UIView+extensionsTests.swift -// -// -// Created by Sergey Kletsov on 29.03.2022. -// - -import XCTest -@testable import Uptech_iOS_Helpers - -final class UIViewExtensionsTests: XCTestCase { - - func testSubviewAdding() throws { - let view = UIView() - let sut = UIView() - let constrains = [sut.heightAnchor.constraint(equalToConstant: 10), - sut.widthAnchor.constraint(equalTo: view.widthAnchor), - sut.centerYAnchor.constraint(equalTo: view.centerYAnchor)] - - view.addSubview(sut, withConstraints: constrains) - - XCTAssertTrue(view.subviews.contains(sut), "view should contain sut") - XCTAssertFalse(sut.translatesAutoresizingMaskIntoConstraints, "sut's translatesAutoresizingMaskIntoConstraints should be false") - XCTAssertTrue(constrains.allSatisfy(\.isActive), "all constrains should be active") - } - - func testSubviewWithEdgeConstraintsAdding() throws { - let frame = CGRect(origin: .zero, size: .init(width: 10, height: 10)) - let view = UIView(frame: frame) - let sut = UIView() - - view.addSubviewWithEdgeConstraints(sut) - sut.layoutIfNeeded() - - XCTAssertEqual(frame, sut.bounds, "sut's bounds should be equal to view's bounds") - } - - func testSubviewInserting() throws { - let view = UIView() - let subview1 = UIView() - let subview2 = UIView() - view.addSubview(subview1) - view.addSubview(subview2) - - let sut = UIView() - let constrains = [sut.heightAnchor.constraint(equalToConstant: 10), - sut.widthAnchor.constraint(equalTo: view.widthAnchor), - sut.centerYAnchor.constraint(equalTo: view.centerYAnchor)] - let index = 1 - view.insertSubview(sut, at: index, withConstraints: constrains) - - XCTAssertEqual(view.subviews.firstIndex(of: sut), index, "sut's index should be \(index)") - XCTAssertFalse(sut.translatesAutoresizingMaskIntoConstraints, "sut's translatesAutoresizingMaskIntoConstraints should be false") - XCTAssertTrue(constrains.allSatisfy(\.isActive), "all constrains should be active") - } - - func testSubviewWithEdgeConstraintsInserting() throws { - let frame = CGRect(origin: .zero, size: .init(width: 10, height: 10)) - let view = UIView(frame: frame) - let subview1 = UIView() - let subview2 = UIView() - view.addSubview(subview1) - view.addSubview(subview2) - - let sut = UIView() - let index = 1 - view.insertSubviewWithEdgeConstraints(sut, at: index) - sut.layoutIfNeeded() - - XCTAssertEqual(view.subviews.firstIndex(of: sut), index, "sut's index should be \(index)") - XCTAssertEqual(frame, sut.bounds, "sut's bounds should be equal to view's bounds") - } - -} diff --git a/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/Collection+indexPathTests.swift b/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/Collection+indexPathTests.swift new file mode 100644 index 0000000..ac051ca --- /dev/null +++ b/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/Collection+indexPathTests.swift @@ -0,0 +1,37 @@ +// +// Collection+indexPathTests.swift +// +// +// Created by Sergey Kletsov on 29.03.2022. +// + +import XCTest +@testable import UptechUIKitHelper + +final class CollectionIndexPathTests: XCTestCase { + private let sut = [[0, 1, 2], + [3, 4, 5], + [6, 7, 8]] + + func testIndexPathSubscriptFirstItem() { + let result = sut[IndexPath(item: 0, section: 0)] + let expectedResult = 0 + + XCTAssertEqual(result, expectedResult) + } + + func testIndexPathSubscriptMiddleItem() { + let result = sut[IndexPath(row: 1, section: 1)] + let expectedResult = 4 + + XCTAssertEqual(result, expectedResult) + } + + func testIndexPathSubscriptLastItem() { + let result = sut[IndexPath(item: 2, section: 2)] + let expectedResult = 8 + + XCTAssertEqual(result, expectedResult) + } + +} diff --git a/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/NSLayoutConstraint+extensionsTests.swift b/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/NSLayoutConstraint+extensionsTests.swift new file mode 100644 index 0000000..9d2c266 --- /dev/null +++ b/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/NSLayoutConstraint+extensionsTests.swift @@ -0,0 +1,36 @@ +// +// NSLayoutConstraint+extensionsTests.swift +// +// +// Created by Sergey Kletsov on 29.03.2022. +// + +import XCTest +@testable import UptechUIKitHelper + +final class NSLayoutConstraintExtensionsTests: XCTestCase { + + func testWithPriority() throws { + let sut = NSLayoutConstraint() + let priority = UILayoutPriority.almostRequired + let result = sut.withPriority(priority) + XCTAssertEqual(result.priority, priority, "sut's priority should be \(priority)") + } + + func testActivateExtension() throws { + let rootView = UIView() + let subview = UIView() + + subview.translatesAutoresizingMaskIntoConstraints = false + rootView.addSubview(subview) + + let sut = [subview.heightAnchor.constraint(equalToConstant: 100), + subview.widthAnchor.constraint(equalTo: rootView.widthAnchor), + subview.centerYAnchor.constraint(equalTo: rootView.centerYAnchor), + subview.centerXAnchor.constraint(equalTo: rootView.centerXAnchor)] + + sut.activate() + XCTAssertTrue(sut.allSatisfy(\.isActive), "all constrains in sut array should be activated") + } + +} diff --git a/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/UICollectionView+extensionsTests.swift b/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/UICollectionView+extensionsTests.swift new file mode 100644 index 0000000..72b5c5d --- /dev/null +++ b/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/UICollectionView+extensionsTests.swift @@ -0,0 +1,33 @@ +// +// UICollectionView+extensionsTests.swift +// +// +// Created by Sergey Kletsov on 05.04.2022. +// + +import XCTest +import UIKit +@testable import UptechUIKitHelper + +final class UICollectionViewExtensionsTests: XCTestCase { + private class TestCell: UICollectionViewCell, ReusableCell { } + private let cellType = TestCell.self + + func testRegisterCorrectType() throws { + let collectionView = createCollectionView() + collectionView.register(cellType) + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: .init(item: 0, section: 0)) + let isCellRightType = cell.isKind(of: cellType) + XCTAssertTrue(isCellRightType, "dequeued cell must be \(cellType)") + } + + func testDequeueNotCrashing() { + let collectionView = createCollectionView() + collectionView.register(cellType, forCellWithReuseIdentifier: cellType.reuseIdentifier) + let _ = collectionView.dequeueReusableCell(for: .init(item: 0, section: 0), cellType: cellType) + } + + private func createCollectionView() -> UICollectionView { + UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout()) + } +} diff --git a/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/UITableView+extensionsTests.swift b/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/UITableView+extensionsTests.swift new file mode 100644 index 0000000..8c7fd68 --- /dev/null +++ b/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/UITableView+extensionsTests.swift @@ -0,0 +1,29 @@ +// +// UITableView+extensionsTests.swift +// +// +// Created by Sergey Kletsov on 05.04.2022. +// + +import XCTest +import UIKit +@testable import UptechUIKitHelper + +final class UITableViewExtensionsTests: XCTestCase { + private class TestCell: UITableViewCell, ReusableCell { } + private let cellType = TestCell.self + + func testRegisterCorrectType() throws { + let tableView = UITableView() + tableView.register(cellType) + let cell = tableView.dequeueReusableCell(withIdentifier: cellType.reuseIdentifier, for: .init(item: 0, section: 0)) + let isCellRightType = cell.isKind(of: cellType) + XCTAssertTrue(isCellRightType, "dequeued cell must be \(cellType)") + } + + func testDequeueNotCrashing() { + let tableView = UITableView() + tableView.register(cellType, forCellReuseIdentifier: cellType.reuseIdentifier) + let _ = tableView.dequeueReusableCell(for: .init(item: 0, section: 0), cellType: cellType) + } +} diff --git a/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/UIView+extensionsTests.swift b/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/UIView+extensionsTests.swift new file mode 100644 index 0000000..4b0c091 --- /dev/null +++ b/Tests/Uptech-iOS-Helpers-Tests/UIKitHelper/UIView+extensionsTests.swift @@ -0,0 +1,74 @@ +// +// UIView+extensionsTests.swift +// +// +// Created by Sergey Kletsov on 29.03.2022. +// + +import XCTest +@testable import UptechUIKitHelper + +final class UIViewExtensionsTests: XCTestCase { + + func testSubviewAdding() throws { + let view = UIView() + let sut = UIView() + let constrains = [sut.heightAnchor.constraint(equalToConstant: 10), + sut.widthAnchor.constraint(equalTo: view.widthAnchor), + sut.centerYAnchor.constraint(equalTo: view.centerYAnchor)] + + view.addSubview(sut, withConstraints: constrains) + + XCTAssertTrue(view.subviews.contains(sut), "view should contain sut") + XCTAssertFalse(sut.translatesAutoresizingMaskIntoConstraints, "sut's translatesAutoresizingMaskIntoConstraints should be false") + XCTAssertTrue(constrains.allSatisfy(\.isActive), "all constrains should be active") + } + + func testSubviewWithEdgeConstraintsAdding() throws { + let frame = CGRect(origin: .zero, size: .init(width: 10, height: 10)) + let view = UIView(frame: frame) + let sut = UIView() + + view.addSubviewWithEdgeConstraints(sut) + sut.layoutIfNeeded() + + XCTAssertEqual(frame, sut.bounds, "sut's bounds should be equal to view's bounds") + } + + func testSubviewInserting() throws { + let view = UIView() + let subview1 = UIView() + let subview2 = UIView() + view.addSubview(subview1) + view.addSubview(subview2) + + let sut = UIView() + let constrains = [sut.heightAnchor.constraint(equalToConstant: 10), + sut.widthAnchor.constraint(equalTo: view.widthAnchor), + sut.centerYAnchor.constraint(equalTo: view.centerYAnchor)] + let index = 1 + view.insertSubview(sut, at: index, withConstraints: constrains) + + XCTAssertEqual(view.subviews.firstIndex(of: sut), index, "sut's index should be \(index)") + XCTAssertFalse(sut.translatesAutoresizingMaskIntoConstraints, "sut's translatesAutoresizingMaskIntoConstraints should be false") + XCTAssertTrue(constrains.allSatisfy(\.isActive), "all constrains should be active") + } + + func testSubviewWithEdgeConstraintsInserting() throws { + let frame = CGRect(origin: .zero, size: .init(width: 10, height: 10)) + let view = UIView(frame: frame) + let subview1 = UIView() + let subview2 = UIView() + view.addSubview(subview1) + view.addSubview(subview2) + + let sut = UIView() + let index = 1 + view.insertSubviewWithEdgeConstraints(sut, at: index) + sut.layoutIfNeeded() + + XCTAssertEqual(view.subviews.firstIndex(of: sut), index, "sut's index should be \(index)") + XCTAssertEqual(frame, sut.bounds, "sut's bounds should be equal to view's bounds") + } + +} diff --git a/Uptech_iOS_Helpers.podspec b/Uptech_iOS_Helpers.podspec index b4406b5..085479c 100644 --- a/Uptech_iOS_Helpers.podspec +++ b/Uptech_iOS_Helpers.podspec @@ -1,12 +1,12 @@ Pod::Spec.new do |s| s.name = 'Uptech_iOS_Helpers' - s.version = '1.0.1' + s.version = '2.0.0' s.summary = 'iOS helper library that contains commonly used code in Uptech iOS projects.' s.homepage = 'https://github.com/uptechteam/Uptech-iOS-Helpers' s.license = { :type => 'MIT', :file => 'LICENSE.md' } s.author = { 'Sergey Kletsov' => 'sergey.kletsov@uptech.team' } s.source = { :git => 'https://github.com/uptechteam/Uptech-iOS-Helpers.git', :tag => s.version.to_s } - s.ios.deployment_target = '11.0' + s.ios.deployment_target = '13.0' s.swift_version = '5.5' s.source_files = 'Sources/Uptech-iOS-Helpers/**/*' end