diff --git a/Package.swift b/Package.swift index 2102b0b..45e3568 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,7 @@ import PackageDescription let package = Package( name: "SwiftExtensions", + platforms: [.iOS(.v15), .macOS(.v12), .watchOS(.v8), .tvOS(.v15)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( @@ -20,7 +21,8 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "SwiftExtensions", - dependencies: []), + dependencies: [], + path: "Sources"), .testTarget( name: "SwiftExtensionsTests", dependencies: ["SwiftExtensions"]), diff --git a/README.md b/README.md index febedfc..6cbcf9b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,30 @@ # SwiftExtensions -A description of this package. +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![SPM compatible](https://img.shields.io/badge/SPM-Compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager/) +[![Swift](https://img.shields.io/badge/Swift-5.6-orange.svg)](https://swift.org) +[![Xcode](https://img.shields.io/badge/Xcode-13.3-blue.svg)](https://developer.apple.com/xcode) + ![Issues](https://img.shields.io/github/issues/ihamadfuad/SwiftExtensions) + ![Releases](https://img.shields.io/github/v/release/ihamadfuad/SwiftExtensions) + +# Sponsor +[![Sponsor](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/nuralme?country.x=BH&locale.x=en_US) + +# AppStoreReviewsAPI + +A Swift 5.6 implementation of native extensions for iOS, macOS, tvOS, watchOS. + +## Installation +### Swift Package Manager (SPM) + +You can use The Swift Package Manager to install SwiftEmailValidator by adding it to your Package.swift file: + + import PackageDescription + + let package = Package( + name: "MyApp", + targets: [], + dependencies: [ + .Package(url: "https://github.com/ihamadfuad/SwiftExtensions.git", .from: "1.0.0") + ] + ) diff --git a/Sources/SwiftExtensions/Extensions/Character.swift b/Sources/SwiftExtensions/Extensions/Character.swift new file mode 100644 index 0000000..d208bb7 --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/Character.swift @@ -0,0 +1,35 @@ +// +// Character.swift +// +// +// Created by Hamad Ali on 14/04/2022. +// + +import Foundation + +public extension Character { + + var isEmoji: Bool { + + let scalarValue = String(self).unicodeScalars.first!.value + + switch scalarValue { + case 0x1F600...0x1F64F, // Emoticons + 0x1F300...0x1F5FF, // Misc Symbols and Pictographs + 0x1F680...0x1F6FF, // Transport and Map + 0x1F1E6...0x1F1FF, // Regional country flags + 0x2600...0x26FF, // Misc symbols + 0x2700...0x27BF, // Dingbats + 0xE0020...0xE007F, // Tags + 0xFE00...0xFE0F, // Variation Selectors + 0x1F900...0x1F9FF, // Supplemental Symbols and Pictographs + 127_000...127_600, // Various asian characters + 65024...65039, // Variation selector + 9100...9300, // Misc items + 8400...8447: // Combining Diacritical Marks for Symbols + return true + default: + return false + } + } +} diff --git a/Sources/SwiftExtensions/Extensions/Collection.swift b/Sources/SwiftExtensions/Extensions/Collection.swift new file mode 100644 index 0000000..f6a41be --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/Collection.swift @@ -0,0 +1,17 @@ +import Foundation + +public extension Collection { + + var hasElements: Bool { + + !isEmpty + } +} + +public extension Collection { + + subscript (safe index: Index) -> Element? { + + indices.contains(index) ? self[index] : nil + } +} diff --git a/Sources/SwiftExtensions/Extensions/Data.swift b/Sources/SwiftExtensions/Extensions/Data.swift new file mode 100644 index 0000000..8365cb4 --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/Data.swift @@ -0,0 +1,19 @@ +// +// Data.swift +// +// +// Created by Hamad Ali on 14/04/2022. +// + +import Foundation + +public extension Data { + + func string(encoding: String.Encoding) -> String? { + String(data: self, encoding: encoding) + } + + func json(options: JSONSerialization.ReadingOptions = []) throws -> Any { + try JSONSerialization.jsonObject(with: self, options: options) + } +} diff --git a/Sources/SwiftExtensions/Extensions/Date.swift b/Sources/SwiftExtensions/Extensions/Date.swift new file mode 100644 index 0000000..650e5a0 --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/Date.swift @@ -0,0 +1,233 @@ +// +// Date.swift +// +// +// Created by Hamad Ali on 14/04/2022. +// + +import Foundation + +/// Convenience comparable +public extension Date { + + func isBetween(_ startDate: Date, _ endDate: Date, includeBounds: Bool = false) -> Bool { + + if includeBounds { + return startDate.compare(self).rawValue * compare(endDate).rawValue >= 0 + } + + return startDate.compare(self).rawValue * compare(endDate).rawValue > 0 + } + + var isInFuture: Bool { + self > Date() + } + + var isInPast: Bool { + self < Date() + } + + var isInToday: Bool { + Calendar.current.isDateInToday(self) + } + + var isInYesterday: Bool { + Calendar.current.isDateInYesterday(self) + } + + var isInTomorrow: Bool { + Calendar.current.isDateInTomorrow(self) + } + + var isInCurrentWeek: Bool { + Calendar.current.isDate(self, equalTo: Date(), toGranularity: .weekOfYear) + } + + var isInCurrentMonth: Bool { + Calendar.current.isDate(self, equalTo: Date(), toGranularity: .month) + } + + var isInCurrentYear: Bool { + return Calendar.current.isDate(self, equalTo: Date(), toGranularity: .year) + } +} + +/// Convenience get +public extension Date { + + /// Number of day in the current week. + var weekday: Int { + return Calendar.current.component(.weekday, from: self) + } + + /// Number of week in the month. + var weekOfMonth: Int { + return Calendar.current.component(.weekOfMonth, from: self) + } + + /// Number of week in the year. + var weekOfYear: Int { + return Calendar.current.component(.weekOfYear, from: self) + } + + var yesterday: Date { + Calendar.current.date(byAdding: .day, value: -1, to: self) ?? Date() + } + + var tomorrow: Date { + Calendar.current.date(byAdding: .day, value: 1, to: self) ?? Date() + } +} + +/// Convenience set/get +public extension Date { + + /// Get and set second + var second: Int { + + get { + return Calendar.current.component(.second, from: self) + } + set { + let allowedRange = Calendar.current.range(of: .second, in: .minute, for: self)! + guard allowedRange.contains(newValue) else { return } + + let currentSeconds = Calendar.current.component(.second, from: self) + let secondsToAdd = newValue - currentSeconds + if let date = Calendar.current.date(byAdding: .second, value: secondsToAdd, to: self) { + self = date + } + } + } + + /// Get and set minute + var minute: Int { + + get { + return Calendar.current.component(.minute, from: self) + } + set { + + let allowedRange = Calendar.current.range(of: .minute, in: .hour, for: self)! + + guard allowedRange.contains(newValue) + else { + return + } + + let currentMinutes = Calendar.current.component(.minute, from: self) + let minutesToAdd = newValue - currentMinutes + + if let date = Calendar.current.date(byAdding: .minute, value: minutesToAdd, to: self) { + + self = date + } + } + } + + /// Get and set hour + var hour: Int { + + get { + return Calendar.current.component(.hour, from: self) + } + set { + + let allowedRange = Calendar.current.range(of: .hour, in: .day, for: self)! + + guard allowedRange.contains(newValue) + else { + return + } + + let currentHour = Calendar.current.component(.hour, from: self) + let hoursToAdd = newValue - currentHour + + if let date = Calendar.current.date(byAdding: .hour, value: hoursToAdd, to: self) { + + self = date + } + } + } + + /// Get and set day + var day: Int { + + get { + return Calendar.current.component(.day, from: self) + } + set { + + let allowedRange = Calendar.current.range(of: .day, in: .month, for: self)! + + guard allowedRange.contains(newValue) + else { + return + } + + let currentDay = Calendar.current.component(.day, from: self) + let daysToAdd = newValue - currentDay + + if let date = Calendar.current.date(byAdding: .day, value: daysToAdd, to: self) { + + self = date + } + } + } + + /// Get and set month + var month: Int { + + get { + return Calendar.current.component(.month, from: self) + } + set { + + let allowedRange = Calendar.current.range(of: .month, in: .year, for: self)! + + guard allowedRange.contains(newValue) + else { + return + } + + let currentMonth = Calendar.current.component(.month, from: self) + let monthsToAdd = newValue - currentMonth + + if let date = Calendar.current.date(byAdding: .month, value: monthsToAdd, to: self) { + + self = date + } + } + } + + /// Get and set year + var year: Int { + + get { + return Calendar.current.component(.year, from: self) + } + set { + + guard newValue > 0 + else { + return + } + + let currentYear = Calendar.current.component(.year, from: self) + let yearsToAdd = newValue - currentYear + + if let date = Calendar.current.date(byAdding: .year, value: yearsToAdd, to: self) { + + self = date + } + } + } +} + +/// Convenience calendar +public extension Date { + + mutating func add(_ component: Calendar.Component, value: Int) { + self = Calendar.current.date(byAdding: component, value: value, to: self)! + } +} diff --git a/Sources/SwiftExtensions/Extensions/Debounced.swift b/Sources/SwiftExtensions/Extensions/Debounced.swift new file mode 100644 index 0000000..09fe3d4 --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/Debounced.swift @@ -0,0 +1,14 @@ +import Foundation + +public func debounced(delay: TimeInterval, + queue: DispatchQueue = .main, + action: @escaping (() -> Void)) -> () -> Void { + + var workItem: DispatchWorkItem? + + return { + workItem?.cancel() + workItem = DispatchWorkItem(block: action) + queue.asyncAfter(deadline: .now() + delay, execute: workItem!) + } +} diff --git a/Sources/SwiftExtensions/Extensions/DispatchGroup.swift b/Sources/SwiftExtensions/Extensions/DispatchGroup.swift new file mode 100644 index 0000000..56a6015 --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/DispatchGroup.swift @@ -0,0 +1,17 @@ +import Foundation + +public func dispatcher(enter: (VoidBlock) -> Void, _ main: @escaping VoidBlock) { + + let dispatchGroup = DispatchGroup() + + dispatchGroup.enter() + + enter({ + + dispatchGroup.leave() + }) + + dispatchGroup.notify(queue: .main) { + main() + } +} diff --git a/Sources/SwiftExtensions/Extensions/Expirable.swift b/Sources/SwiftExtensions/Extensions/Expirable.swift new file mode 100644 index 0000000..0dd22aa --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/Expirable.swift @@ -0,0 +1,28 @@ +import Foundation + +public struct Expirable { + + private var innerValue: T + private(set) var expirationDate: Date + + public var value: T? { + return hasExpired() ? nil : innerValue + } + + public init(value: T, expirationDate: Date) { + + innerValue = value + self.expirationDate = expirationDate + } + + public init(value: T, duration: Double) { + + innerValue = value + expirationDate = Date().addingTimeInterval(duration) + } + + public func hasExpired() -> Bool { + + expirationDate < Date() + } +} diff --git a/Sources/SwiftExtensions/Extensions/History.swift b/Sources/SwiftExtensions/Extensions/History.swift new file mode 100644 index 0000000..59c1655 --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/History.swift @@ -0,0 +1,26 @@ +import Foundation + +@propertyWrapper +public struct History { + + private var value: Value + private(set) var history: [Value] = [] + + public init(wrappedValue: Value) { + self.value = wrappedValue + } + + public var wrappedValue: Value { + + get { value } + + set { + history.append(value) + value = newValue + } + } + + public var projectedValue: Self { + return self + } +} diff --git a/Sources/SwiftExtensions/Extensions/InlineCondition.swift b/Sources/SwiftExtensions/Extensions/InlineCondition.swift new file mode 100644 index 0000000..1a6caaf --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/InlineCondition.swift @@ -0,0 +1,6 @@ +import Foundation + +public func condition(_ block: () -> T) -> T { + + block() +} diff --git a/Sources/SwiftExtensions/Extensions/Keypath.swift b/Sources/SwiftExtensions/Extensions/Keypath.swift new file mode 100644 index 0000000..b0b36f7 --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/Keypath.swift @@ -0,0 +1,8 @@ +import Foundation + +prefix operator ~> + +prefix public func ~> (_ keyPath: KeyPath) -> (Element) -> Attribute { + + return { element in element[keyPath: keyPath] } +} diff --git a/Sources/SwiftExtensions/Extensions/Optionals.swift b/Sources/SwiftExtensions/Extensions/Optionals.swift new file mode 100644 index 0000000..74b2efa --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/Optionals.swift @@ -0,0 +1,70 @@ +import Foundation + +infix operator ??=: AssignmentPrecedence + +public func && (lhs: Bool?, rhs: Bool?) -> Bool { + + switch (lhs, rhs) { + case (false, _), (_, false): + return false + case let (unwrapLhs?, unwrapRhs?): + return unwrapLhs && unwrapRhs + default: + return false + } +} + +public func || (lhs: Bool?, rhs: Bool?) -> Bool { + + switch (lhs, rhs) { + case (true, _), (_, true): + return true + case let (unwrapLhs?, unwrapRhs?): + return unwrapLhs || unwrapRhs + default: + return false + } +} + +public extension Optional { + + func unwrapped(or defaultValue: Wrapped) -> Wrapped { + + return self ?? defaultValue + } + + /// Assign an optional value to a variable only if the value is not nil + static func ??= (lhs: inout Optional, rhs: Optional) { + guard let rhs = rhs else { return } + lhs = rhs + } +} + +public extension Optional where Wrapped == String { + + var `default`: String { + switch self { + case .some(let value): + return value + case .none: + return "" + } + } +} + +extension Optional: Comparable where Wrapped: Comparable { + + public static func < (lhs: Optional, rhs: Optional) -> Bool { + + switch (lhs, rhs) { + case let (lhs?, rhs?): + return lhs < rhs + case (nil, _?): + return true // anything is greater than nil + case (_?, nil): + return false // nil in smaller than anything + case (nil, nil): + return true // nil is not smaller than itself + } + } +} diff --git a/Sources/SwiftExtensions/Extensions/Sequence.swift b/Sources/SwiftExtensions/Extensions/Sequence.swift new file mode 100644 index 0000000..6971dd6 --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/Sequence.swift @@ -0,0 +1,97 @@ +import Foundation + +public extension Sequence where Element: Equatable { + + func removeDuplicates() -> [Element] { + + reduce([], { $0.contains($1) ? $0 : $0 + [$1] }) + } +} + +public extension Sequence { + + func sorted(by attribute: KeyPath) -> [Element] { + + sorted(by: { $0[keyPath: attribute] < $1[keyPath: attribute] }) + } +} + +/** + struct Person { + var name: String + var age: Int + } + + let mike = Person(name: "Mike", age: 18) + let john = Person(name: "John", age: 18) + let bob = Person(name: "Bob", age: 56) + let jake = Person(name: "Jake", age: 56) + let roman = Person(name: "Roman", age: 25) + + let persons = [mike, john, bob, jake, roman] + + let groupedPersons = persons.group { $0.age } + + for persons in groupedPersons { + print(persons.map { $0.name }) + } + */ +public extension Sequence { + + func group(by key: (Iterator.Element) -> GroupingType) -> [[Iterator.Element]] { + + var groups: [GroupingType: [Iterator.Element]] = [:] + var groupsOrder: [GroupingType] = [] + + forEach { element in + + let key = key(element) + + if case nil = groups[key]?.append(element) { + + groups[key] = [element] + groupsOrder.append(key) + } + } + + return groupsOrder.map { groups[$0]! } + } +} + +public extension Array where Element: Equatable { + + mutating func remove(_ object: Element) throws { + + if let index = firstIndex(of: object) { + + remove(at: index) + + return + } + + throw "Object doesn't exist." + } +} + +public extension Array where Element: Hashable { + + func next(item: Element) -> Element? { + + if let index = firstIndex(of: item), indices.contains(index + 1) { + + return self[index + 1] + } + + return nil + } + + func previous(item: Element) -> Element? { + + if let index = firstIndex(of: item), indices.contains(index - 1) { + + return self[index - 1] + } + + return nil + } +} diff --git a/Sources/SwiftExtensions/Extensions/String.swift b/Sources/SwiftExtensions/Extensions/String.swift new file mode 100644 index 0000000..753a625 --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/String.swift @@ -0,0 +1,69 @@ +import Foundation + +extension String: Error { } + +extension String: LocalizedError { + + public var errorDescription: String? { + return self + } +} + +public extension String { + + var words: [String] { + + components(separatedBy: .punctuationCharacters) + .joined() + .components(separatedBy: .whitespaces) + .filter{!$0.isEmpty} + } + + /// CamelCase of string. + /// + /// "hEllO woRlD".camelCased -> "helloWorld" + /// + var camelCased: String { + + let source = lowercased() + let first = source[.. String { + + guard hasPrefix(prefix) + else { + return self + } + + return String(dropFirst(prefix.count)) + } + + func removingSuffix(_ suffix: String) -> String { + + guard hasSuffix(suffix) + else { + return self + } + + return String(dropLast(suffix.count)) + } + + func withPrefix(_ prefix: String) -> String { + + guard !hasPrefix(prefix) + else { + return self + } + + return prefix + self + } +} diff --git a/Sources/SwiftExtensions/Extensions/Typealias.swift b/Sources/SwiftExtensions/Extensions/Typealias.swift new file mode 100644 index 0000000..386af4e --- /dev/null +++ b/Sources/SwiftExtensions/Extensions/Typealias.swift @@ -0,0 +1,6 @@ +import Foundation + +public typealias VoidBlock = () -> Void +public typealias SuccessBlock = (_ success: Bool) -> Void +public typealias FailureBlock = (_ error: Error) -> Void +public typealias ResultBlock = (_ success: Bool, _ error: Error) -> Void diff --git a/Sources/SwiftExtensions/SwiftExtensions.swift b/Sources/SwiftExtensions/SwiftExtensions.swift deleted file mode 100644 index 6a9ca9b..0000000 --- a/Sources/SwiftExtensions/SwiftExtensions.swift +++ /dev/null @@ -1,6 +0,0 @@ -public struct SwiftExtensions { - public private(set) var text = "Hello, World!" - - public init() { - } -} diff --git a/Tests/SwiftExtensionsTests/SwiftExtensionsTests.swift b/Tests/SwiftExtensionsTests/SwiftExtensionsTests.swift index f80fcfb..f660db2 100644 --- a/Tests/SwiftExtensionsTests/SwiftExtensionsTests.swift +++ b/Tests/SwiftExtensionsTests/SwiftExtensionsTests.swift @@ -6,6 +6,6 @@ final class SwiftExtensionsTests: XCTestCase { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - XCTAssertEqual(SwiftExtensions().text, "Hello, World!") + XCTAssertEqual("Hello, World!", "Hello, World!") } }