diff --git a/ACKategories.podspec b/ACKategories.podspec index 935d031f..450d7cb0 100644 --- a/ACKategories.podspec +++ b/ACKategories.podspec @@ -31,6 +31,6 @@ Tools, cocoa subclasses and extensions we love to use at Ackee. s.source_files = 'ACKategories/Classes/**/*' - # s.frameworks = 'UIKit', 'MapKit' + s.frameworks = 'UIKit' # s.dependency 'AFNetworking', '~> 2.3' end diff --git a/ACKategories.xcodeproj/project.pbxproj b/ACKategories.xcodeproj/project.pbxproj index c9026093..17280515 100644 --- a/ACKategories.xcodeproj/project.pbxproj +++ b/ACKategories.xcodeproj/project.pbxproj @@ -7,6 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + 6964365B1FDAF72A000D5CAA /* UIViewController+SafeAreaCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6964365A1FDAF72A000D5CAA /* UIViewController+SafeAreaCompat.swift */; }; + 6965AEE91FDB5752001A08C5 /* ConditionalAssignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6965AEE81FDB5752001A08C5 /* ConditionalAssignment.swift */; }; + 69A646BE1FD1AE1600BD4A98 /* SafeSubscript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A646B91FD1AE1600BD4A98 /* SafeSubscript.swift */; }; + 69A646BF1FD1AE1600BD4A98 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A646BA1FD1AE1600BD4A98 /* FoundationExtensions.swift */; }; + 69A646C01FD1AE1600BD4A98 /* UIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A646BB1FD1AE1600BD4A98 /* UIImageExtensions.swift */; }; + 69A646C11FD1AE1600BD4A98 /* UISearchBarExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A646BC1FD1AE1600BD4A98 /* UISearchBarExtensions.swift */; }; + 69A646C21FD1AE1600BD4A98 /* UserDefaultsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A646BD1FD1AE1600BD4A98 /* UserDefaultsExtensions.swift */; }; D253FF901F0A65A80079215C /* Buttons+FixedIntrinsicContentSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253FF871F0A65A80079215C /* Buttons+FixedIntrinsicContentSize.swift */; }; D253FF911F0A65A80079215C /* Color+Extra.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253FF881F0A65A80079215C /* Color+Extra.swift */; }; D253FF921F0A65A80079215C /* Control+Blocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253FF891F0A65A80079215C /* Control+Blocks.swift */; }; @@ -17,6 +24,13 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 6964365A1FDAF72A000D5CAA /* UIViewController+SafeAreaCompat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+SafeAreaCompat.swift"; sourceTree = ""; }; + 6965AEE81FDB5752001A08C5 /* ConditionalAssignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionalAssignment.swift; sourceTree = ""; }; + 69A646B91FD1AE1600BD4A98 /* SafeSubscript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SafeSubscript.swift; sourceTree = ""; }; + 69A646BA1FD1AE1600BD4A98 /* FoundationExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationExtensions.swift; sourceTree = ""; }; + 69A646BB1FD1AE1600BD4A98 /* UIImageExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtensions.swift; sourceTree = ""; }; + 69A646BC1FD1AE1600BD4A98 /* UISearchBarExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UISearchBarExtensions.swift; sourceTree = ""; }; + 69A646BD1FD1AE1600BD4A98 /* UserDefaultsExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtensions.swift; sourceTree = ""; }; D253FF781F0A65610079215C /* ACKategories.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ACKategories.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D253FF841F0A65A80079215C /* .gitkeep */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .gitkeep; sourceTree = ""; }; D253FF871F0A65A80079215C /* Buttons+FixedIntrinsicContentSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Buttons+FixedIntrinsicContentSize.swift"; sourceTree = ""; }; @@ -77,11 +91,18 @@ children = ( D253FF871F0A65A80079215C /* Buttons+FixedIntrinsicContentSize.swift */, D253FF881F0A65A80079215C /* Color+Extra.swift */, + 6965AEE81FDB5752001A08C5 /* ConditionalAssignment.swift */, D253FF891F0A65A80079215C /* Control+Blocks.swift */, + 69A646BA1FD1AE1600BD4A98 /* FoundationExtensions.swift */, + 69A646B91FD1AE1600BD4A98 /* SafeSubscript.swift */, D253FF8A1F0A65A80079215C /* String+Extra.swift */, D253FF8B1F0A65A80079215C /* TableAndCollectionViewExtensions.swift */, D253FF8C1F0A65A80079215C /* TableHeaderFooterView.swift */, D253FF8D1F0A65A80079215C /* UIControlEvents.swift */, + 69A646BB1FD1AE1600BD4A98 /* UIImageExtensions.swift */, + 69A646BC1FD1AE1600BD4A98 /* UISearchBarExtensions.swift */, + 6964365A1FDAF72A000D5CAA /* UIViewController+SafeAreaCompat.swift */, + 69A646BD1FD1AE1600BD4A98 /* UserDefaultsExtensions.swift */, ); path = Classes; sourceTree = ""; @@ -167,11 +188,18 @@ files = ( D253FF911F0A65A80079215C /* Color+Extra.swift in Sources */, D253FF901F0A65A80079215C /* Buttons+FixedIntrinsicContentSize.swift in Sources */, + 6964365B1FDAF72A000D5CAA /* UIViewController+SafeAreaCompat.swift in Sources */, + 69A646C21FD1AE1600BD4A98 /* UserDefaultsExtensions.swift in Sources */, + 69A646C11FD1AE1600BD4A98 /* UISearchBarExtensions.swift in Sources */, D253FF961F0A65A80079215C /* UIControlEvents.swift in Sources */, D253FF921F0A65A80079215C /* Control+Blocks.swift in Sources */, + 69A646BE1FD1AE1600BD4A98 /* SafeSubscript.swift in Sources */, D253FF931F0A65A80079215C /* String+Extra.swift in Sources */, D253FF941F0A65A80079215C /* TableAndCollectionViewExtensions.swift in Sources */, + 69A646C01FD1AE1600BD4A98 /* UIImageExtensions.swift in Sources */, D253FF951F0A65A80079215C /* TableHeaderFooterView.swift in Sources */, + 69A646BF1FD1AE1600BD4A98 /* FoundationExtensions.swift in Sources */, + 6965AEE91FDB5752001A08C5 /* ConditionalAssignment.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ACKategories/Classes/ConditionalAssignment.swift b/ACKategories/Classes/ConditionalAssignment.swift new file mode 100644 index 00000000..5d2f92ab --- /dev/null +++ b/ACKategories/Classes/ConditionalAssignment.swift @@ -0,0 +1,15 @@ +import Foundation + +precedencegroup ConditionalAssignmentPrecedence { + associativity: left + assignment: true + higherThan: AssignmentPrecedence +} + +infix operator =?: ConditionalAssignmentPrecedence + +public func =?(variable: inout T, value: T?) { + if let v = value { + variable = v + } +} diff --git a/ACKategories/Classes/FoundationExtensions.swift b/ACKategories/Classes/FoundationExtensions.swift index 5b1d3c08..cfbc0242 100644 --- a/ACKategories/Classes/FoundationExtensions.swift +++ b/ACKategories/Classes/FoundationExtensions.swift @@ -61,3 +61,29 @@ extension NumberFormatter { return self.string(from: NSNumber(value: number)) } } + +public func + (lhs: [Key: Value], rhs: [Key: Value]) -> [Key: Value] { + var result = lhs + for (k, v) in rhs { result.updateValue(v, forKey: k) } + return result +} + +extension Array where Element: Equatable { + public mutating func remove(object: Element) { + if let index = index(of: object) { + remove(at: index) + } + } +} + +extension Bundle { + public var receiptData: Data? { return appStoreReceiptURL.flatMap { try? Data(contentsOf: $0) } } + public var version: String? { return infoDictionary?["CFBundleShortVersionString"] as? String } + public var buildNumber: Int? { return (infoDictionary?["CFBundleVersion"] as? String).flatMap { Int($0) } } +} + +extension TimeInterval { + public static var minute: TimeInterval { return TimeInterval(60) } + public static var hour: TimeInterval { return minute * 60 } + public static var day: TimeInterval { return hour * 24 } +} diff --git a/ACKategories/Classes/SafeSubscript.swift b/ACKategories/Classes/SafeSubscript.swift new file mode 100644 index 00000000..c2666825 --- /dev/null +++ b/ACKategories/Classes/SafeSubscript.swift @@ -0,0 +1,12 @@ +import Foundation + +/// Safe subscript for collections +public protocol SafeRandomAccessCollection: RandomAccessCollection { + subscript(safe index: Int) -> Iterator.Element? { get } +} + +extension Array: SafeRandomAccessCollection { + public subscript(safe index: Int) -> Iterator.Element? { + return indices ~= index ? self[index] : nil + } +} diff --git a/ACKategories/Classes/TableAndCollectionViewExtensions.swift b/ACKategories/Classes/TableAndCollectionViewExtensions.swift index 85783ab6..48acd185 100644 --- a/ACKategories/Classes/TableAndCollectionViewExtensions.swift +++ b/ACKategories/Classes/TableAndCollectionViewExtensions.swift @@ -3,14 +3,13 @@ import UIKit public protocol Reusable { } extension Reusable { - public static var reuseIdentifier: String { return NSStringFromClass(self as! AnyObject.Type) } - } extension UITableViewCell: Reusable { } +extension UITableViewHeaderFooterView: Reusable { } extension UICollectionReusableView: Reusable { } extension UITableView { @@ -18,6 +17,11 @@ extension UITableView { register(T.classForCoder(), forCellReuseIdentifier: T.reuseIdentifier) return dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T } + + public func dequeueHeaderFooterView() -> T where T: UITableViewHeaderFooterView { + register(T.classForCoder(), forHeaderFooterViewReuseIdentifier: T.reuseIdentifier) + return dequeueReusableHeaderFooterView(withIdentifier: T.reuseIdentifier) as! T + } } extension UICollectionView { @@ -25,4 +29,9 @@ extension UICollectionView { register(T.classForCoder(), forCellWithReuseIdentifier: T.reuseIdentifier) return dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath) as! T } + + public func dequeueSupplementaryView(ofKind kind: String, for indexPath: IndexPath) -> T where T: UICollectionReusableView { + register(T.classForCoder(), forSupplementaryViewOfKind: kind, withReuseIdentifier: T.reuseIdentifier) + return dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: T.reuseIdentifier, for: indexPath) as! T + } } diff --git a/ACKategories/Classes/UIImageExtensions.swift b/ACKategories/Classes/UIImageExtensions.swift new file mode 100644 index 00000000..39b6cd74 --- /dev/null +++ b/ACKategories/Classes/UIImageExtensions.swift @@ -0,0 +1,34 @@ +import UIKit + +extension UIImage { + + // taken from http://stackoverflow.com/questions/10850184/ios-image-get-rotated-90-degree-after-saved-as-png-representation-data + public func fixedOrientation() -> UIImage { + guard imageOrientation != .up else { return self } + + UIGraphicsBeginImageContextWithOptions(size, false, scale) + draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) + let normalizedImage = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + + return normalizedImage + } + + public func resized(maxDimension: CGFloat) -> UIImage? { + let isLandscape = size.width > size.height + + let newSize: CGSize + if isLandscape { + newSize = CGSize(width: maxDimension, height: (size.height / size.width) * maxDimension) + } else { + newSize = CGSize(width: (size.width / size.height) * maxDimension, height: maxDimension) + } + + UIGraphicsBeginImageContextWithOptions(newSize, false, scale) + draw(in: CGRect(origin: .zero, size: newSize)) + let newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return newImage + } + +} diff --git a/ACKategories/Classes/UISearchBarExtensions.swift b/ACKategories/Classes/UISearchBarExtensions.swift new file mode 100644 index 00000000..655c3c92 --- /dev/null +++ b/ACKategories/Classes/UISearchBarExtensions.swift @@ -0,0 +1,5 @@ +import UIKit + +public extension UISearchBar { + public var textField: UITextField! { return value(forKey: "searchField") as! UITextField } +} diff --git a/ACKategories/Classes/UIViewController+SafeAreaCompat.swift b/ACKategories/Classes/UIViewController+SafeAreaCompat.swift new file mode 100644 index 00000000..11401bd8 --- /dev/null +++ b/ACKategories/Classes/UIViewController+SafeAreaCompat.swift @@ -0,0 +1,30 @@ +import UIKit + +@available(iOS 9.0, *) +extension UIViewController { + private enum Keys { + static var safeArea: UInt8 = 0 + } + + /// Layout guide compatibility extension for iOS 11 safe area + /// + /// On iOS 11+ is the same as `view.safeAreaLayoutGuide`. + /// + /// On older systems it fallbacks to `topLayoutGuide.bottom` and `bottomLayoutGuide.top`, side constraints are equal to superview. + public var safeArea: UILayoutGuide { + if #available(iOS 11.0, *) { + return view.safeAreaLayoutGuide + } else { + if let layoutGuide = objc_getAssociatedObject(self, &Keys.safeArea) as? UILayoutGuide { return layoutGuide } + + let layoutGuide = UILayoutGuide() + view.addLayoutGuide(layoutGuide) + layoutGuide.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor).isActive = true + layoutGuide.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true + layoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + layoutGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + objc_setAssociatedObject(self, &Keys.safeArea, layoutGuide, .OBJC_ASSOCIATION_ASSIGN) + return layoutGuide + } + } +} diff --git a/ACKategories/Classes/UserDefaultsExtensions.swift b/ACKategories/Classes/UserDefaultsExtensions.swift new file mode 100644 index 00000000..7d177e52 --- /dev/null +++ b/ACKategories/Classes/UserDefaultsExtensions.swift @@ -0,0 +1,20 @@ +import Foundation + +extension UserDefaults { + private enum Keys { + static let deviceID = "ud_device_id_b8cb6644-43fa-4bc4-a4f3-23f9e5d25c8f" + } + + public var deviceID: String { + if let result = string(forKey: Keys.deviceID) { + return result + } + + let newDeviceID = NSUUID().uuidString + + set(newDeviceID, forKey: Keys.deviceID) + synchronize() + + return newDeviceID + } +} diff --git a/Example/Podfile.lock b/Example/Podfile.lock index e7f8dfb2..bd40c1b6 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -13,7 +13,7 @@ EXTERNAL SOURCES: :path: ../ SPEC CHECKSUMS: - ACKategories: 139a869c79dcc12d6f6debd53898f3d169b8dd4e + ACKategories: 6634e7cbdaf5b3593367469c958ecfa24eaaec4b Nimble: bfe1f814edabba69ff145cb1283e04ed636a67f2 Quick: 5d290df1c69d5ee2f0729956dcf0fd9a30447eaa