diff --git a/Sources/ExtensionKit/CoreGraphics/CGPoint.swift b/Sources/ExtensionKit/CoreGraphics/CGPoint.swift new file mode 100644 index 0000000..ff6a753 --- /dev/null +++ b/Sources/ExtensionKit/CoreGraphics/CGPoint.swift @@ -0,0 +1,16 @@ +import CoreGraphics + +public extension CGPoint { + + /// Offset point by new x and y + /// - Parameters: + /// - x: x + /// - y: y + /// - Returns: new point + func offseted(x: CGFloat = 0.0, y: CGFloat = 0.0) -> CGPoint { + var point = self + point.x += x + point.y += y + return point + } +} diff --git a/Sources/ExtensionKit/CoreGraphics/CGRect.swift b/Sources/ExtensionKit/CoreGraphics/CGRect.swift new file mode 100644 index 0000000..6b12379 --- /dev/null +++ b/Sources/ExtensionKit/CoreGraphics/CGRect.swift @@ -0,0 +1,64 @@ +import CoreGraphics + +public extension CGRect { + + var topLeft: CGPoint { + return origin + } + + var topRight: CGPoint { + return CGPoint(x: maxX, y: minY) + } + + var topMiddle: CGPoint { + return CGPoint(x: midX, y: minY) + } + + var bottomLeft: CGPoint { + return CGPoint(x: minX, y: maxY) + } + + var bottomRight: CGPoint { + return CGPoint(x: maxX, y: maxY) + } + + var bottomMiddle: CGPoint { + return CGPoint(x: midX, y: maxY) + } + + var leftMiddle: CGPoint { + return CGPoint(x: minX, y: midY) + } + + var rightMiddle: CGPoint { + return CGPoint(x: maxX, y: midY) + } + + var midX: CGFloat { + return (maxX - minX) / 2 + } + + var midY: CGFloat { + return (maxY - minY) / 2 + } + + /// Center taking size into account + var center: CGPoint { + get { + let x = origin.x + size.width / 2 + let y = origin.y + size.height / 2 + return CGPoint(x: x, y: y) + } + set { + origin.x = newValue.x - size.width / 2 + origin.y = newValue.y - size.height / 2 + } + } + + var sameCenterSquare: CGRect { + let maxLength = max(size.width, size.height) + var rect = CGRect(x: 0, y: 0, width: maxLength, height: maxLength) + rect.center = center + return rect + } +} diff --git a/Sources/ExtensionKit/CoreGraphics/Operators.swift b/Sources/ExtensionKit/CoreGraphics/Operators.swift new file mode 100644 index 0000000..8288702 --- /dev/null +++ b/Sources/ExtensionKit/CoreGraphics/Operators.swift @@ -0,0 +1,17 @@ +import CoreGraphics + +public func +(lhs: CGSize, rhs: CGSize) -> CGSize { + return CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height) +} + +public func -(lhs: CGSize, rhs: CGSize) -> CGSize { + return CGSize(width: lhs.width - rhs.width, height: lhs.height - rhs.height) +} + +public func *(lhs: CGSize, rhs: CGFloat) -> CGSize { + return CGSize(width: lhs.width * rhs, height: lhs.height * rhs) +} + +public func /(lhs: CGSize, rhs: CGFloat) -> CGSize { + return CGSize(width: lhs.width / rhs, height: lhs.height / rhs) +} diff --git a/Sources/ExtensionKit/ExtensionKit.swift b/Sources/ExtensionKit/ExtensionKit.swift deleted file mode 100644 index 1143b2c..0000000 --- a/Sources/ExtensionKit/ExtensionKit.swift +++ /dev/null @@ -1,3 +0,0 @@ -struct ExtensionKit { - var text = "Hello, World!" -} diff --git a/Sources/ExtensionKit/Foundation/Array.swift b/Sources/ExtensionKit/Foundation/Array.swift new file mode 100644 index 0000000..bfa3d38 --- /dev/null +++ b/Sources/ExtensionKit/Foundation/Array.swift @@ -0,0 +1,27 @@ +import Foundation + +public extension Array where Element: Equatable { + + /// Removes the given element in the array. + /// + /// - Parameter element: The element to be removed. + /// - Returns: The element got removed, or `nil` if the element doesn't exist. + @discardableResult + mutating func remove(_ element: Element) -> Element? { + if let index = self.firstIndex(of: element) { + return self.remove(at: index) + } + return nil + } + + /// Returns an array where repeating elements of the receiver got removed. + var removingRepeatElements: Array { + var arr = Array() + forEach { + if !arr.contains($0) { + arr.append($0) + } + } + return arr + } +} diff --git a/Sources/ExtensionKit/Foundation/CFRunLoopTimer.swift b/Sources/ExtensionKit/Foundation/CFRunLoopTimer.swift new file mode 100644 index 0000000..d7344c0 --- /dev/null +++ b/Sources/ExtensionKit/Foundation/CFRunLoopTimer.swift @@ -0,0 +1,10 @@ +import Foundation + +public extension CFRunLoopTimer { + + /// Invalidate CFRunLoopTimer + func invalidate() { + CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), self, .commonModes) + } + +} diff --git a/Sources/ExtensionKit/Foundation/Collection.swift b/Sources/ExtensionKit/Foundation/Collection.swift new file mode 100644 index 0000000..503193d --- /dev/null +++ b/Sources/ExtensionKit/Foundation/Collection.swift @@ -0,0 +1,13 @@ +import Foundation + +public extension Collection { + + /// Safe indexing. + /// Returns the element at the specified index iff it is within bounds, otherwise nil. + /// ref: https://stackoverflow.com/questions/25329186/safe-bounds-checked-array-lookup-in-swift-through-optional-bindings + /// + /// - Parameter index: The index used to retrieve a value / an object. + subscript (safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} diff --git a/Sources/ExtensionKit/Foundation/Data.swift b/Sources/ExtensionKit/Foundation/Data.swift new file mode 100644 index 0000000..18aba2a --- /dev/null +++ b/Sources/ExtensionKit/Foundation/Data.swift @@ -0,0 +1,11 @@ +import Foundation + +public extension Data { + /// Returns a string of hex value. + var hexString: String { + return withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> String in + let buffer = bytes.bindMemory(to: UInt8.self) + return buffer.map {String(format: "%02hhx", $0)}.reduce("", { $0 + $1 }) + } + } +} diff --git a/Sources/ExtensionKit/Foundation/Date.swift b/Sources/ExtensionKit/Foundation/Date.swift new file mode 100644 index 0000000..6d77754 --- /dev/null +++ b/Sources/ExtensionKit/Foundation/Date.swift @@ -0,0 +1,15 @@ +import Foundation + +public extension Date { + + /// Days of the week in Gregorian calendar (Sunday - Saturday) + static let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + + /// Current day of the week in Gregorian calendar + var day: String { + let gregorian = Calendar(identifier: .gregorian) + let weekday = gregorian.component(.weekday, from: self) + return Date.days[weekday] + } + +} diff --git a/Sources/ExtensionKit/Foundation/FileManager.swift b/Sources/ExtensionKit/Foundation/FileManager.swift new file mode 100644 index 0000000..ab67477 --- /dev/null +++ b/Sources/ExtensionKit/Foundation/FileManager.swift @@ -0,0 +1,67 @@ +import Foundation + +public extension FileManager { + + /// Size of file at path + /// + /// - Parameter path: file path + /// - Returns: Size in bytes + func fileSize(atPath path: String) -> Int { + let attributes = try? attributesOfItem(atPath: path) + return (attributes?[FileAttributeKey.size] as? Int) ?? 0 + } + + /// Size of folder + /// + /// - Parameter path: folder path + /// - Returns: size in bytes + func folderSize(atPath path: String) -> Int { + let manager = FileManager.default + if manager.fileExists(atPath: path) { + do { + let childFilesEnumerator = try (manager.subpathsOfDirectory(atPath: path) as NSArray).objectEnumerator() + var folderSize = 0 + while childFilesEnumerator.nextObject() != nil { + if let fileName = childFilesEnumerator.nextObject() as? String, + let url = URL(string: path) { + let fileAbsolutePath = url.appendingPathComponent(fileName).absoluteString + folderSize += self.fileSize(atPath: fileAbsolutePath) + } + } + return folderSize + + } catch { + dprint(error) + } + } + return 0 + } + + + /// Size of directory at URL + /// + /// - Parameter URL: URL + /// - Returns: Size in bytes + func directorySize(at URL: URL) -> Int { + var result = 0 + let properties = [URLResourceKey.localizedNameKey, URLResourceKey.creationDateKey, URLResourceKey.localizedTypeDescriptionKey] + let manager = FileManager.default + do { + let urls = try manager.contentsOfDirectory(at: URL, includingPropertiesForKeys: properties, options: .skipsHiddenFiles) + for fileSystemItem in urls { + var directory: ObjCBool = false + let path = fileSystemItem.path + manager.fileExists(atPath: path, isDirectory: &directory) + if directory.boolValue { + result += directorySize(at: fileSystemItem) + } else { + result += try manager.attributesOfItem(atPath: path)[FileAttributeKey.size] as! Int + } + } + + } catch { + dprint("Error: \(error)") + } + return result + } +} diff --git a/Sources/ExtensionKit/Foundation/Global.swift b/Sources/ExtensionKit/Foundation/Global.swift new file mode 100644 index 0000000..10cddf8 --- /dev/null +++ b/Sources/ExtensionKit/Foundation/Global.swift @@ -0,0 +1,30 @@ +import Foundation + +/// AppVersion set in `CFBundleShortVersionString` +public var appVersion: String { + return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String +} + +/// Languages set in user defaults key `AppleLanguages` +public var systemLanguages: [String] { + return UserDefaults.standard.stringArray(forKey: "AppleLanguages") ?? [] +} + +/// Sleeps the thread +/// - Parameter duration: in seconds +public func sleep(duration: TimeInterval) { + #if DEBUG + Thread.sleep(forTimeInterval: duration) + #endif +} + +/// Swift still calls `print()` and/or `debugPrint()` in shipped apps. +/// We use a method described in onevcat's post (https://onevcat.com/2016/02/swift-performance/) +/// to optimaze the performance. +/// +/// - Parameter item: items to print +public func dprint(_ item: @autoclosure () -> Any) { + #if DEBUG + print(item()) + #endif +} diff --git a/Sources/ExtensionKit/Foundation/Int.swift b/Sources/ExtensionKit/Foundation/Int.swift new file mode 100644 index 0000000..c9b0c11 --- /dev/null +++ b/Sources/ExtensionKit/Foundation/Int.swift @@ -0,0 +1,72 @@ +import Foundation + +public extension Int { + + /// Whether self is an odd number + var isOdd: Bool { + return !isEven + } + + /// Whether self is an even number + var isEven: Bool { + return self % 2 == 0 + } + + /// Treats 0 as nil + var nilIfZero: Int? { + if self == 0 { return nil } + return self + } + + /// Make the number to string + var string: String { + return String(self) + } + + /// Make a range from zero to self + var range: CountableRange { + return 0..(of creation: @autoclosure () throws -> T) rethrows -> [T] { + return try (0 ..< self).map { _ in + try creation() + } + } + + /// Return if `self` is in the given range. + /// + /// - Parameter range: Target range. + /// - Returns: `true` if self is in the range, otherwise `false`. + func inRange(_ range: Range) -> Bool { + return range.contains(self) + } + + + // minutes to seconds + var minutes: Int { + return self * 60 + } + + // hours to seconds + var hours: Int { + return (self * 60).minutes + } + + var days: Int { + return (self * 24).hours + } + + var months: Int { + return (self * 30).days + } + + var years: Int { + return (self * 12).months + } + +} diff --git a/Sources/ExtensionKit/Foundation/NSObject.swift b/Sources/ExtensionKit/Foundation/NSObject.swift new file mode 100644 index 0000000..c94709b --- /dev/null +++ b/Sources/ExtensionKit/Foundation/NSObject.swift @@ -0,0 +1,31 @@ +import Foundation + +public extension NSObject { + + /// Exchange two implementations of the given selectors, aka method swizzling. + /// + /// - Parameters: + /// - originalSelector: The original selector. + /// - swizzledSelector: Another selector. + class func exchangeImplementations(originalSelector: Selector, swizzledSelector: Selector) { + guard + let originalMethod = class_getInstanceMethod(self, originalSelector), + let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) + else { + dprint("Error: Unable to exchange method implemenation!!") + return + } + method_exchangeImplementations(originalMethod, swizzledMethod) + } + + /// Return class name. + var className: String { + return type(of: self).description().components(separatedBy: ".").last ?? "" + } + + /// Print the deinitialization message of self. + final func printDeinitMessage() { + dprint("Deinit Message: \(className): \(self)") + } + +} diff --git a/Sources/ExtensionKit/Foundation/String.swift b/Sources/ExtensionKit/Foundation/String.swift new file mode 100644 index 0000000..13095af --- /dev/null +++ b/Sources/ExtensionKit/Foundation/String.swift @@ -0,0 +1,80 @@ +import UIKit + +public extension String { + + /// Is valid email + var isEmail: Bool { + let emailRegax = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" + let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegax) + return emailTest.evaluate(with: self) + } + + /// Are numbers 0-9 + var areNumbers: Bool { + let allnumRegex = "^[0-9]*$" + let numPredicate = NSPredicate(format: "SELF MATCHES %@", allnumRegex) + return numPredicate.evaluate(with: self) + } + + /// Cast to Int + var toInt: Int? { Int(self) } + + /// Cast to Double + var toDouble: Double? { Double(self) } + + /// Trimming ".0" + var trimmingZeroDecimal: String { + if let double = Double(self), double.truncatingRemainder(dividingBy: 1) == 0 { + return String(format: "%.0f", double) + } + return self + } + + /// Adding "+" at the very beginning. + var addingPlusSymbol: String { "+" + self } + + /// Adding "-" at the very beginning. + var addingMinusSymbol: String { "-" + self } + + /// Returns the CGSize that the string being layout on screen. + /// + /// - Parameter font: The given font. + /// - Returns: The result CGSize. + func layoutSize(with font: UIFont) -> CGSize { + self.size(withAttributes: [.font: font]) + } + + /// Cast as Int and add the given value. No changes if casting fails. + mutating func advanceNumberValue(step: Int = 1) { + if let value = Int(self) { + self = String(value.advanced(by: step)) + } + } + + /// Comparing app versions. Returns `true` if self is `1.1.0` and the given value is `1.2.0`. + /// - Parameter aVersion: Another version. + /// - Returns: `true` if the give version is newer than self. + func isOldAppVersion(comparedWith aVersion: String) -> Bool { + self.compare(aVersion, options: .numeric) == .orderedAscending + } + + var uppercasingFirstLetter: String { + prefix(1).uppercased() + dropFirst() + } + + var lowercasingFirstLetter: String { + prefix(1).lowercased() + dropFirst() + } + + /// Return `true` if self is empty or only contains white spaces and/or new lines. + var isBlank: Bool { isEmpty || trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + + /// Return `false` if self is empty or only contains white spaces and/or new lines. + var isVisible: Bool { !isBlank } + + /// Return `nil` if `self.isBlank` is `true`. + var nilIfBlank: String? { isBlank ? nil : self } + + @available(*, deprecated, message: "Use `nilIfBlank` instead.") + func treatsVisuallyEmptyAsNil() -> String? { nilIfBlank } +} diff --git a/Sources/ExtensionKit/Foundation/Timer.swift b/Sources/ExtensionKit/Foundation/Timer.swift new file mode 100644 index 0000000..404340b --- /dev/null +++ b/Sources/ExtensionKit/Foundation/Timer.swift @@ -0,0 +1,38 @@ +import Foundation + +public extension Timer { + + /// Schedule closure to run on main run loop after delay + /// + /// - Parameters: + /// - delay: Delay interval + /// - handler: Closure to run + /// - Returns: `CFRunLoopTimer` + @discardableResult + class func schedule(delay: TimeInterval, handler: @escaping () -> Void) -> CFRunLoopTimer? { + let fireDate = delay + CFAbsoluteTimeGetCurrent() + let timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, fireDate, 0, 0, 0) { _ in + handler() + } + CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, CFRunLoopMode.commonModes) + return timer + } + + + /// Schedule closure to run on main run loop and repeat at the interval + /// + /// - Parameters: + /// - interval: 触发时长,从现在开始经过interval时长后触发第一次 + /// - handler: Closure to run + /// - Returns: CFRunLoopTimer + @discardableResult + class func schedule(repeatInterval interval: TimeInterval, handler: @escaping () -> Void) -> CFRunLoopTimer? { + let fireDate = interval + CFAbsoluteTimeGetCurrent() + let timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, fireDate, interval, 0, 0) { _ in + handler() + } + CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, CFRunLoopMode.commonModes) + return timer + } + +} diff --git a/Tests/ExtensionKitTests/ExtensionKitTests.swift b/Tests/ExtensionKitTests/ExtensionKitTests.swift index 8fc4af9..06bfb4a 100644 --- a/Tests/ExtensionKitTests/ExtensionKitTests.swift +++ b/Tests/ExtensionKitTests/ExtensionKitTests.swift @@ -6,7 +6,7 @@ final class ExtensionKitTests: XCTestCase { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - XCTAssertEqual(ExtensionKit().text, "Hello, World!") +// XCTAssertEqual(ExtensionKit().text, "Hello, World!") } static var allTests = [