├── .github └── workflows │ ├── build.yml │ ├── lint.yml │ └── release.yml ├── .gitignore ├── Brewfile ├── Brewfile.lock.json ├── CHANGELOG.md ├── Documentation ├── .gitkeep └── Reference │ └── ExtensionKit │ ├── README.md │ ├── classes │ └── Subscribers.LimitedSink.md │ ├── enums │ ├── CLLocation.GeocodeError.md │ ├── CLLocationManager.AuthorizationType.md │ ├── Height.md │ ├── PrintEvent.md │ ├── UIImage.ArrowDirection.md │ └── UIView.GradientLayerChangingDirection.md │ ├── extensions │ ├── Array.md │ ├── Binding.md │ ├── Bundle.md │ ├── Button.md │ ├── CFRunLoopTimer.md │ ├── CGPoint.md │ ├── CGRect.md │ ├── CLLocation.md │ ├── CLLocationCoordinate2D.md │ ├── CLLocationManager.md │ ├── Collection.md │ ├── Color.md │ ├── Data.md │ ├── Date.md │ ├── FileManager.md │ ├── GeometryProxy.md │ ├── Image.md │ ├── Int.md │ ├── NSObject.md │ ├── Publisher.md │ ├── Publishers.Autoconnect.md │ ├── RandomAccessCollection.md │ ├── Shape.md │ ├── String.Index.md │ ├── String.md │ ├── Text.md │ ├── Timer.md │ ├── UIApplication.md │ ├── UIButton.md │ ├── UIColor.md │ ├── UIImage.md │ ├── UITableView.md │ ├── UIView.md │ ├── UIViewController.md │ ├── URL.md │ ├── UserDefaults.md │ └── View.md │ ├── methods │ ├── +(____).md │ ├── -(____).md │ ├── _(____).md │ ├── dprint(__).md │ ├── dprint(____).md │ └── sleep(duration_).md │ ├── protocols │ └── TimerStartable.md │ ├── structs │ ├── KeyboardInfo.md │ ├── Notification.KeyboardInfo.md │ ├── Preview.md │ ├── Screen.md │ └── UserDefault.md │ └── typealiases │ ├── Subscribers.ReceivedCompletion.md │ └── Subscribers.ReceivedValue.md ├── Gemfile ├── Gemfile.lock ├── Package.swift ├── README.md ├── Sources ├── .gitkeep └── ExtensionKit │ ├── AutoLayout │ ├── LayoutAnchor.swift │ ├── LayoutClosure.swift │ ├── LayoutDimension.swift │ ├── LayoutOperators.swift │ ├── LayoutProperty.swift │ ├── LayoutProxy.swift │ ├── LayoutSizeDimension.swift │ └── UIView+Fill.swift │ ├── Combine │ ├── Publisher.swift │ └── Subscriber.swift │ ├── CoreGraphics │ ├── CGAffineTransform.swift │ ├── CGPoint.swift │ ├── CGRect.swift │ ├── CGSize+Extensions.swift │ └── Operators.swift │ ├── CoreLocation │ ├── AuthorizationPublisher.swift │ ├── CLLocation.swift │ ├── CLLocationCoordinate2D.swift │ ├── CLLocationManager.swift │ └── LocationPublisher.swift │ ├── Foundation │ ├── Apply.swift │ ├── Array.swift │ ├── Bundle.swift │ ├── CFRunLoopTimer.swift │ ├── Calendar.swift │ ├── Collection.swift │ ├── Data.swift │ ├── Date.swift │ ├── DispactQueue.swift │ ├── FileManager.swift │ ├── Global.swift │ ├── Int.swift │ ├── NSObject.swift │ ├── Notification.swift │ ├── RandomAccessCollection.swift │ ├── String.swift │ ├── Timer.swift │ ├── URL.swift │ └── UserDefaults.swift │ ├── PropertyWrappers │ └── UserDefault.swift │ ├── SwiftUI │ ├── Array+View.swift │ ├── Binding.swift │ ├── Button.swift │ ├── Color.swift │ ├── Geometry │ │ ├── GeometryProxy.swift │ │ └── RectGetter.swift │ ├── Image.swift │ ├── Keyboard │ │ └── KeyboardInfo.swift │ ├── MailView.swift │ ├── MessageView.swift │ ├── RoundedCorner.swift │ ├── SafariView.swift │ ├── Scroll │ │ └── ScrollViewOffSetReader.swift │ ├── Shape.swift │ ├── StoreView.swift │ ├── Text.swift │ └── View.swift │ ├── Transition │ ├── SharedTransitioning.swift │ ├── TransitionAnimationController.swift │ └── TransitionType.swift │ ├── UIKit │ ├── CALayer.swift │ ├── UIApplication.swift │ ├── UIButton.swift │ ├── UICollectionReusableView+Extensions.swift │ ├── UICollectionView+Extensions.swift │ ├── UIColor.swift │ ├── UIDevice.swift │ ├── UIImage.swift │ ├── UIScreen.swift │ ├── UITableView.swift │ ├── UIView.swift │ ├── UIViewController.swift │ └── ViewControllerIdentifiable.swift │ └── Utils │ ├── Constants.swift │ ├── PHAsset+Extensions.swift │ ├── RandomColor.swift │ └── Then.swift ├── Tests ├── ExtensionKitTests │ ├── ExtensionKitTests.swift │ └── XCTestManifests.swift └── LinuxMain.swift ├── fastlane ├── Fastfile └── README.md ├── lgtm.sh ├── lint-config.js └── package.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | name: Swift build 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | types: [ opened, edited, reopened, synchronize ] 9 | paths: 10 | - '**.swift' 11 | jobs: 12 | build: 13 | name: Swift build 14 | runs-on: macos-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: build 18 | run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios14.0-simulator" -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Commitlint 2 | on: 3 | pull_request: 4 | branches: [ develop, main, release/*, release-* ] 5 | types: [ opened, edited, reopened, synchronize ] 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - uses: wagoid/commitlint-github-action@v1 17 | with: 18 | configFile: lint-config.js -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | 2 | name: New Version Release 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: [closed] 8 | jobs: 9 | template: 10 | if: github.event.pull_request.merged == true 11 | name: New Version Release 12 | runs-on: macos-latest 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | ref: main 19 | - name: Version bump & update docs 20 | run: | 21 | brew bundle 22 | npm install 23 | bundle install 24 | ./update.sh -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | 7 | .swiftpm/xcode/package.xcworkspace/ 8 | 9 | *.xml 10 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "sourcedocs" 2 | -------------------------------------------------------------------------------- /Brewfile.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": { 3 | "brew": { 4 | "sourcedocs": { 5 | "version": "2.0.1", 6 | "bottle": { 7 | "rebuild": 0, 8 | "root_url": "https://ghcr.io/v2/homebrew/core", 9 | "files": { 10 | "arm64_sonoma": { 11 | "cellar": ":any_skip_relocation", 12 | "url": "https://ghcr.io/v2/homebrew/core/sourcedocs/blobs/sha256:5461899a0c97729c247703f45eb98a0cf26fc9939ce50eb6fa9543e9f70c6c62", 13 | "sha256": "5461899a0c97729c247703f45eb98a0cf26fc9939ce50eb6fa9543e9f70c6c62" 14 | }, 15 | "arm64_ventura": { 16 | "cellar": ":any_skip_relocation", 17 | "url": "https://ghcr.io/v2/homebrew/core/sourcedocs/blobs/sha256:fbe4a3e5c3485101486be0639b81cc4799c2dd7e0edf5f528d32a3c0ca6122fa", 18 | "sha256": "fbe4a3e5c3485101486be0639b81cc4799c2dd7e0edf5f528d32a3c0ca6122fa" 19 | }, 20 | "arm64_monterey": { 21 | "cellar": ":any_skip_relocation", 22 | "url": "https://ghcr.io/v2/homebrew/core/sourcedocs/blobs/sha256:b8757a91d73d96999da362afbc5a5c42c7be949f562cf5569b2bf24853af6ef9", 23 | "sha256": "b8757a91d73d96999da362afbc5a5c42c7be949f562cf5569b2bf24853af6ef9" 24 | }, 25 | "arm64_big_sur": { 26 | "cellar": ":any_skip_relocation", 27 | "url": "https://ghcr.io/v2/homebrew/core/sourcedocs/blobs/sha256:1254fb0f47a037f929e579b4a68dd375b0e587d9adb3e876865b6de031d39f46", 28 | "sha256": "1254fb0f47a037f929e579b4a68dd375b0e587d9adb3e876865b6de031d39f46" 29 | }, 30 | "sonoma": { 31 | "cellar": ":any_skip_relocation", 32 | "url": "https://ghcr.io/v2/homebrew/core/sourcedocs/blobs/sha256:9378990038e4d444ca11820d4e41eee737522dc75b7818d7577f4a612ab18bf1", 33 | "sha256": "9378990038e4d444ca11820d4e41eee737522dc75b7818d7577f4a612ab18bf1" 34 | }, 35 | "ventura": { 36 | "cellar": ":any_skip_relocation", 37 | "url": "https://ghcr.io/v2/homebrew/core/sourcedocs/blobs/sha256:5cf36d5bfe2a9a2770454f21169e5e9c8a5a6b50525ec0a9418c88180706d40c", 38 | "sha256": "5cf36d5bfe2a9a2770454f21169e5e9c8a5a6b50525ec0a9418c88180706d40c" 39 | }, 40 | "monterey": { 41 | "cellar": ":any_skip_relocation", 42 | "url": "https://ghcr.io/v2/homebrew/core/sourcedocs/blobs/sha256:974904c0b5b4d0d54fe8392c84fe06b3aa23e47fb76f95579f09e5fc94704d2d", 43 | "sha256": "974904c0b5b4d0d54fe8392c84fe06b3aa23e47fb76f95579f09e5fc94704d2d" 44 | }, 45 | "big_sur": { 46 | "cellar": ":any_skip_relocation", 47 | "url": "https://ghcr.io/v2/homebrew/core/sourcedocs/blobs/sha256:292dbf6713d17716e685ac74c0e9fdbe07038b95bca36f234a94bfe2fffe5aab", 48 | "sha256": "292dbf6713d17716e685ac74c0e9fdbe07038b95bca36f234a94bfe2fffe5aab" 49 | }, 50 | "catalina": { 51 | "cellar": ":any_skip_relocation", 52 | "url": "https://ghcr.io/v2/homebrew/core/sourcedocs/blobs/sha256:56cad5d1e01271614fd93c5ec93b4b7fc7cabb64bef767581bc5ad179ee20a63", 53 | "sha256": "56cad5d1e01271614fd93c5ec93b4b7fc7cabb64bef767581bc5ad179ee20a63" 54 | } 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | "system": { 61 | "macos": { 62 | "sequoia": { 63 | "HOMEBREW_VERSION": "4.3.18", 64 | "HOMEBREW_PREFIX": "/opt/homebrew", 65 | "Homebrew/homebrew-core": "api", 66 | "CLT": "16.0.0.0.1.1723780521", 67 | "Xcode": "16.0", 68 | "macOS": "15.1" 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Documentation/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtokman/ExtensionKit/6198a989e661416a64f7523710bcf07c67e7e2e6/Documentation/.gitkeep -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/README.md: -------------------------------------------------------------------------------- 1 | # Reference Documentation 2 | 3 | ## Protocols 4 | 5 | - [TimerStartable](protocols/TimerStartable.md) 6 | 7 | ## Structs 8 | 9 | - [KeyboardInfo](structs/KeyboardInfo.md) 10 | - [Preview](structs/Preview.md) 11 | - [Screen](structs/Screen.md) 12 | - [UserDefault](structs/UserDefault.md) 13 | 14 | ## Classes 15 | 16 | - [Subscribers.LimitedSink](classes/Subscribers.LimitedSink.md) 17 | 18 | ## Enums 19 | 20 | - [CLLocation.GeocodeError](enums/CLLocation.GeocodeError.md) 21 | - [Height](enums/Height.md) 22 | - [PrintEvent](enums/PrintEvent.md) 23 | - [UIImage.ArrowDirection](enums/UIImage.ArrowDirection.md) 24 | - [UIView.GradientLayerChangingDirection](enums/UIView.GradientLayerChangingDirection.md) 25 | 26 | ## Extensions 27 | 28 | - [Array](extensions/Array.md) 29 | - [Binding](extensions/Binding.md) 30 | - [Bundle](extensions/Bundle.md) 31 | - [Button](extensions/Button.md) 32 | - [CFRunLoopTimer](extensions/CFRunLoopTimer.md) 33 | - [CGPoint](extensions/CGPoint.md) 34 | - [CGRect](extensions/CGRect.md) 35 | - [CLLocation](extensions/CLLocation.md) 36 | - [CLLocationCoordinate2D](extensions/CLLocationCoordinate2D.md) 37 | - [CLLocationManager](extensions/CLLocationManager.md) 38 | - [Collection](extensions/Collection.md) 39 | - [Color](extensions/Color.md) 40 | - [Data](extensions/Data.md) 41 | - [Date](extensions/Date.md) 42 | - [FileManager](extensions/FileManager.md) 43 | - [GeometryProxy](extensions/GeometryProxy.md) 44 | - [Image](extensions/Image.md) 45 | - [Int](extensions/Int.md) 46 | - [NSObject](extensions/NSObject.md) 47 | - [Publisher](extensions/Publisher.md) 48 | - [Publishers.Autoconnect](extensions/Publishers.Autoconnect.md) 49 | - [RandomAccessCollection](extensions/RandomAccessCollection.md) 50 | - [Shape](extensions/Shape.md) 51 | - [String](extensions/String.md) 52 | - [String.Index](extensions/String.Index.md) 53 | - [Text](extensions/Text.md) 54 | - [Timer](extensions/Timer.md) 55 | - [UIApplication](extensions/UIApplication.md) 56 | - [UIButton](extensions/UIButton.md) 57 | - [UIColor](extensions/UIColor.md) 58 | - [UIImage](extensions/UIImage.md) 59 | - [UITableView](extensions/UITableView.md) 60 | - [UIView](extensions/UIView.md) 61 | - [UIViewController](extensions/UIViewController.md) 62 | - [URL](extensions/URL.md) 63 | - [UserDefaults](extensions/UserDefaults.md) 64 | - [View](extensions/View.md) 65 | 66 | ## Typealiases 67 | 68 | - [Subscribers.ReceivedCompletion](typealiases/Subscribers.ReceivedCompletion.md) 69 | - [Subscribers.ReceivedValue](typealiases/Subscribers.ReceivedValue.md) 70 | 71 | ## Methods 72 | 73 | - [+(____)](methods/+(____).md) 74 | - [-(____)](methods/-(____).md) 75 | - [_(____)](methods/_(____).md) 76 | - [_(____)](methods/_(____).md) 77 | - [dprint(____)](methods/dprint(____).md) 78 | - [sleep(duration_)](methods/sleep(duration_).md) 79 | 80 | This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs) on 2021-09-20 22:30:24 +0000 -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/classes/Subscribers.LimitedSink.md: -------------------------------------------------------------------------------- 1 | **CLASS** 2 | 3 | # `Subscribers.LimitedSink` 4 | 5 | ```swift 6 | class LimitedSink: Subscriber, Cancellable where Failure: Error 7 | ``` 8 | 9 | ## Methods 10 | ### `receive(subscription:)` 11 | 12 | ```swift 13 | public func receive(subscription: Subscription) 14 | ``` 15 | 16 | #### Parameters 17 | 18 | | Name | Description | 19 | | ---- | ----------- | 20 | | subscription | A subscription that represents the connection between publisher and subscriber. | 21 | 22 | ### `receive(_:)` 23 | 24 | ```swift 25 | public func receive(_ input: Input) -> Subscribers.Demand 26 | ``` 27 | 28 | #### Parameters 29 | 30 | | Name | Description | 31 | | ---- | ----------- | 32 | | input | The published element. | 33 | 34 | ### `receive(completion:)` 35 | 36 | ```swift 37 | public func receive(completion: Subscribers.Completion) 38 | ``` 39 | 40 | #### Parameters 41 | 42 | | Name | Description | 43 | | ---- | ----------- | 44 | | completion | A `Subscribers/Completion` case indicating whether publishing completed normally or with an error. | 45 | 46 | ### `cancel()` 47 | 48 | ```swift 49 | public func cancel() 50 | ``` 51 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/enums/CLLocation.GeocodeError.md: -------------------------------------------------------------------------------- 1 | **ENUM** 2 | 3 | # `CLLocation.GeocodeError` 4 | 5 | ```swift 6 | enum GeocodeError: Error 7 | ``` 8 | 9 | Geocode location `Error` 10 | 11 | ## Cases 12 | ### `invalid(_:)` 13 | 14 | ```swift 15 | case invalid(String) 16 | ``` 17 | 18 | ### `empty(_:)` 19 | 20 | ```swift 21 | case empty(String) 22 | ``` 23 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/enums/CLLocationManager.AuthorizationType.md: -------------------------------------------------------------------------------- 1 | **ENUM** 2 | 3 | # `CLLocationManager.AuthorizationType` 4 | 5 | ```swift 6 | enum AuthorizationType: String 7 | ``` 8 | 9 | Authorization access level 10 | 11 | ## Cases 12 | ### `always` 13 | 14 | ```swift 15 | case always 16 | ``` 17 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/enums/Height.md: -------------------------------------------------------------------------------- 1 | **ENUM** 2 | 3 | # `Height` 4 | 5 | ```swift 6 | public enum Height: Equatable 7 | ``` 8 | 9 | Bottom sheet modal height 10 | 11 | ## Cases 12 | ### `low` 13 | 14 | ```swift 15 | case low 16 | ``` 17 | 18 | Third of Screen size 19 | 20 | ### `mid` 21 | 22 | ```swift 23 | case mid 24 | ``` 25 | 26 | Half of Screen size 27 | 28 | ### `full` 29 | 30 | ```swift 31 | case full 32 | ``` 33 | 34 | Full Screen size 35 | 36 | ### `custom(_:)` 37 | 38 | ```swift 39 | case custom(CGFloat) 40 | ``` 41 | 42 | Custom screen size 43 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/enums/PrintEvent.md: -------------------------------------------------------------------------------- 1 | **ENUM** 2 | 3 | # `PrintEvent` 4 | 5 | ```swift 6 | public enum PrintEvent: String 7 | ``` 8 | 9 | Print event type 10 | 11 | ## Cases 12 | ### `e` 13 | 14 | ```swift 15 | case e = "[‼️]" 16 | ``` 17 | 18 | Error 19 | 20 | ### `i` 21 | 22 | ```swift 23 | case i = "[ℹ️]" 24 | ``` 25 | 26 | Info 27 | 28 | ### `d` 29 | 30 | ```swift 31 | case d = "[💬]" 32 | ``` 33 | 34 | Debug 35 | 36 | ### `v` 37 | 38 | ```swift 39 | case v = "[🔬]" 40 | ``` 41 | 42 | Verbose 43 | 44 | ### `w` 45 | 46 | ```swift 47 | case w = "[⚠️]" 48 | ``` 49 | 50 | Warning 51 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/enums/UIImage.ArrowDirection.md: -------------------------------------------------------------------------------- 1 | **ENUM** 2 | 3 | # `UIImage.ArrowDirection` 4 | 5 | ```swift 6 | enum ArrowDirection 7 | ``` 8 | 9 | ## Cases 10 | ### `up` 11 | 12 | ```swift 13 | case up 14 | ``` 15 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/enums/UIView.GradientLayerChangingDirection.md: -------------------------------------------------------------------------------- 1 | **ENUM** 2 | 3 | # `UIView.GradientLayerChangingDirection` 4 | 5 | ```swift 6 | enum GradientLayerChangingDirection 7 | ``` 8 | 9 | ## Cases 10 | ### `top` 11 | 12 | ```swift 13 | case top 14 | ``` 15 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Array.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Array` 4 | ```swift 5 | public extension Array where Element: Equatable 6 | ``` 7 | 8 | ## Properties 9 | ### `removingRepeatElements` 10 | 11 | ```swift 12 | var removingRepeatElements: Array 13 | ``` 14 | 15 | Returns an array where repeating elements of the receiver got removed. 16 | 17 | ### `body` 18 | 19 | ```swift 20 | public var body: some View 21 | ``` 22 | 23 | Add View conformance to Array where element is View 24 | 25 | ## Methods 26 | ### `remove(_:)` 27 | 28 | ```swift 29 | mutating func remove(_ element: Element) -> Element? 30 | ``` 31 | 32 | Removes the given element in the array. 33 | 34 | - Parameter element: The element to be removed. 35 | - Returns: The element got removed, or `nil` if the element doesn't exist. 36 | 37 | #### Parameters 38 | 39 | | Name | Description | 40 | | ---- | ----------- | 41 | | element | The element to be removed. | 42 | 43 | ### `init(_:)` 44 | 45 | ```swift 46 | init(_ dictionary: [String: T]) 47 | ``` 48 | 49 | Initialize `URLQueryItem from dictionary` 50 | - Parameter dictionary: url parameters 51 | 52 | #### Parameters 53 | 54 | | Name | Description | 55 | | ---- | ----------- | 56 | | dictionary | url parameters | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Binding.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Binding` 4 | ```swift 5 | public extension Binding where Value: Equatable 6 | ``` 7 | 8 | ## Methods 9 | ### `onChange(_:)` 10 | 11 | ```swift 12 | func onChange(_ action: @escaping (Value) -> Void) -> Self 13 | ``` 14 | 15 | ``` 16 | Toggle( 17 | "Foo", 18 | isOn: $foo.onChange { 19 | bar.isEnabled = $0 20 | } 21 | ) 22 | ``` 23 | 24 | #### Parameters 25 | 26 | | Name | Description | 27 | | ---- | ----------- | 28 | | action | Action to run | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Bundle.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Bundle` 4 | ```swift 5 | public extension Bundle 6 | ``` 7 | 8 | ## Properties 9 | ### `versionNumber` 10 | 11 | ```swift 12 | var versionNumber: String 13 | ``` 14 | 15 | Version number via - CFBundleShortVersionString 16 | 17 | ### `buildNumber` 18 | 19 | ```swift 20 | var buildNumber: String 21 | ``` 22 | 23 | Build number via - CFBundleVersion 24 | 25 | ## Methods 26 | ### `decode(_:fromFile:withExtension:dateDecodingStrategy:keyDecodingStrategy:)` 27 | 28 | ```swift 29 | func decode( 30 | _ type: T.Type, 31 | fromFile file: String, 32 | withExtension `extension`: String? = nil, 33 | dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate, 34 | keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys 35 | ) -> Deferred> 36 | ``` 37 | 38 | Load model type from bundle resource 39 | - Parameters: 40 | - type: Type to load 41 | - file: File name 42 | - dateDecodingStrategy: date decoding strategy 43 | - keyDecodingStrategy: key decoding strategy 44 | - Returns: Future 45 | 46 | #### Parameters 47 | 48 | | Name | Description | 49 | | ---- | ----------- | 50 | | type | Type to load | 51 | | file | File name | 52 | | dateDecodingStrategy | date decoding strategy | 53 | | keyDecodingStrategy | key decoding strategy | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Button.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Button` 4 | ```swift 5 | public extension Button where Label == Image 6 | ``` 7 | 8 | ## Methods 9 | ### `init(systemName:action:)` 10 | 11 | ```swift 12 | init(systemName: String, action: @escaping () -> Void) 13 | ``` 14 | 15 | Initialize button with system image and trailing action 16 | - Parameters: 17 | - systemName: System image name 18 | - action: action 19 | 20 | #### Parameters 21 | 22 | | Name | Description | 23 | | ---- | ----------- | 24 | | systemName | System image name | 25 | | action | action | 26 | 27 | ### `init(imageName:action:)` 28 | 29 | ```swift 30 | init(imageName: String, action: @escaping () -> Void) 31 | ``` 32 | 33 | Initialize button with local image and trailing action 34 | - Parameters: 35 | - systemName: System image name 36 | - action: action 37 | 38 | #### Parameters 39 | 40 | | Name | Description | 41 | | ---- | ----------- | 42 | | systemName | System image name | 43 | | action | action | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/CFRunLoopTimer.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `CFRunLoopTimer` 4 | ```swift 5 | public extension CFRunLoopTimer 6 | ``` 7 | 8 | ## Methods 9 | ### `invalidate()` 10 | 11 | ```swift 12 | func invalidate() 13 | ``` 14 | 15 | Invalidate CFRunLoopTimer 16 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/CGPoint.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `CGPoint` 4 | ```swift 5 | public extension CGPoint 6 | ``` 7 | 8 | ## Methods 9 | ### `offseted(x:y:)` 10 | 11 | ```swift 12 | func offseted(x: CGFloat = 0.0, y: CGFloat = 0.0) -> CGPoint 13 | ``` 14 | 15 | Offset point by new x and y 16 | - Parameters: 17 | - x: x 18 | - y: y 19 | - Returns: new point 20 | 21 | #### Parameters 22 | 23 | | Name | Description | 24 | | ---- | ----------- | 25 | | x | x | 26 | | y | y | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/CGRect.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `CGRect` 4 | ```swift 5 | public extension CGRect 6 | ``` 7 | 8 | ## Properties 9 | ### `topLeft` 10 | 11 | ```swift 12 | var topLeft: CGPoint 13 | ``` 14 | 15 | ### `topRight` 16 | 17 | ```swift 18 | var topRight: CGPoint 19 | ``` 20 | 21 | ### `topMiddle` 22 | 23 | ```swift 24 | var topMiddle: CGPoint 25 | ``` 26 | 27 | ### `bottomLeft` 28 | 29 | ```swift 30 | var bottomLeft: CGPoint 31 | ``` 32 | 33 | ### `bottomRight` 34 | 35 | ```swift 36 | var bottomRight: CGPoint 37 | ``` 38 | 39 | ### `bottomMiddle` 40 | 41 | ```swift 42 | var bottomMiddle: CGPoint 43 | ``` 44 | 45 | ### `leftMiddle` 46 | 47 | ```swift 48 | var leftMiddle: CGPoint 49 | ``` 50 | 51 | ### `rightMiddle` 52 | 53 | ```swift 54 | var rightMiddle: CGPoint 55 | ``` 56 | 57 | ### `center` 58 | 59 | ```swift 60 | var center: CGPoint 61 | ``` 62 | 63 | Center taking size into account 64 | 65 | ### `sameCenterSquare` 66 | 67 | ```swift 68 | var sameCenterSquare: CGRect 69 | ``` 70 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/CLLocation.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `CLLocation` 4 | ```swift 5 | public extension CLLocation 6 | ``` 7 | 8 | ## Methods 9 | ### `init(from:)` 10 | 11 | ```swift 12 | convenience init(from coordinate: CLLocationCoordinate2D) 13 | ``` 14 | 15 | `CLLocation` from `CLLocationCoordinate2D` 16 | - Parameter coordinate: `CLLocationCoordinate2D` 17 | 18 | #### Parameters 19 | 20 | | Name | Description | 21 | | ---- | ----------- | 22 | | coordinate | `CLLocationCoordinate2D` | 23 | 24 | ### `reverseGeocode()` 25 | 26 | ```swift 27 | func reverseGeocode() -> Deferred> 28 | ``` 29 | 30 | Reverse geocode a `CLLocation` 31 | - Parameter location: `CLLocation` 32 | - Returns: Future with Result<[`CLPlacemark`], `GeocodeError`> 33 | 34 | #### Parameters 35 | 36 | | Name | Description | 37 | | ---- | ----------- | 38 | | location | `CLLocation` | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/CLLocationCoordinate2D.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `CLLocationCoordinate2D` 4 | ```swift 5 | extension CLLocationCoordinate2D: Equatable 6 | ``` 7 | 8 | ## Methods 9 | ### `==(_:_:)` 10 | 11 | ```swift 12 | public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool 13 | ``` 14 | 15 | Equatable coordinates 16 | 17 | ### `encode(to:)` 18 | 19 | ```swift 20 | public func encode(to encoder: Encoder) throws 21 | ``` 22 | 23 | Encode a `CLLocationCoordinate2D` 24 | - Parameter encoder: Encoder 25 | - Throws: `EncodingError` 26 | 27 | #### Parameters 28 | 29 | | Name | Description | 30 | | ---- | ----------- | 31 | | encoder | Encoder | 32 | 33 | ### `init(from:)` 34 | 35 | ```swift 36 | public init(from decoder: Decoder) throws 37 | ``` 38 | 39 | Decode `CLLocationCoordinate2D` from data 40 | - Parameter decoder: Decoder 41 | - Throws: `DecodingError` 42 | 43 | #### Parameters 44 | 45 | | Name | Description | 46 | | ---- | ----------- | 47 | | decoder | Decoder | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/CLLocationManager.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `CLLocationManager` 4 | ```swift 5 | public extension CLLocationManager 6 | ``` 7 | 8 | ## Methods 9 | ### `requestLocationWhenInUseAuthorization()` 10 | 11 | ```swift 12 | func requestLocationWhenInUseAuthorization() -> AnyPublisher 13 | ``` 14 | 15 | ``` 16 | class LocationStore: ObservableObject { 17 | 18 | @Published var status: CLAuthorizationStatus = .notDetermined 19 | let manager = CLLocationManager() 20 | var cancellables = Set() 21 | 22 | func requestPermission() { 23 | manager 24 | .requestLocationWhenInUseAuthorization() 25 | .assign(to: &$status) 26 | } 27 | } 28 | ``` 29 | 30 | ### `requestLocationAlwaysAuthorization()` 31 | 32 | ```swift 33 | func requestLocationAlwaysAuthorization() -> AnyPublisher 34 | ``` 35 | 36 | ``` 37 | class LocationStore: ObservableObject { 38 | 39 | @Published var status: CLAuthorizationStatus = .notDetermined 40 | let manager = CLLocationManager() 41 | var cancellables = Set() 42 | 43 | func requestPermission() { 44 | manager 45 | .requestLocationWhenInUseAuthorization() 46 | .assign(to: &$status) 47 | } 48 | } 49 | ``` 50 | 51 | ### `receiveLocationUpdates(oneTime:)` 52 | 53 | ```swift 54 | func receiveLocationUpdates(oneTime: Bool = false) -> AnyPublisher<[CLLocation], Error> 55 | ``` 56 | 57 | ``` 58 | class LocationStore: ObservableObject { 59 | 60 | @Published var coordinate: CLLocationCoordinate2D = .zero 61 | let manager = CLLocationManager() 62 | var cancellables = Set() 63 | 64 | func requestLocation() { 65 | manager 66 | .receiveLocationUpdates() 67 | .compactMap(\.last) 68 | .map(\.coordinate) 69 | .replaceError(with: .zero) 70 | .assign(to: &$coordinate) 71 | } 72 | } 73 | ``` 74 | 75 | #### Parameters 76 | 77 | | Name | Description | 78 | | ---- | ----------- | 79 | | oneTime | One time location update or constant updates, default: false | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Collection.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Collection` 4 | ```swift 5 | public extension Collection 6 | ``` 7 | 8 | ## Methods 9 | ### `distance(to:)` 10 | 11 | ```swift 12 | func distance(to index: Index) -> Int 13 | ``` 14 | 15 | Distance to index 16 | - Parameter index: Index 17 | - Returns: Int 18 | 19 | #### Parameters 20 | 21 | | Name | Description | 22 | | ---- | ----------- | 23 | | index | Index | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Color.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Color` 4 | ```swift 5 | public extension Color 6 | ``` 7 | 8 | ## Properties 9 | ### `inverted` 10 | 11 | ```swift 12 | var inverted: Color 13 | ``` 14 | 15 | Inverse color 16 | 17 | ## Methods 18 | ### `hex(_:alpha:)` 19 | 20 | ```swift 21 | static func hex(_ hex: UInt, alpha: Double = 1) -> Self 22 | ``` 23 | 24 | Initialize with HEX 25 | - Parameter hex: HEX UInt value 26 | - Returns: Color 27 | 28 | #### Parameters 29 | 30 | | Name | Description | 31 | | ---- | ----------- | 32 | | hex | HEX UInt value | 33 | 34 | ### `hex(_:alpha:)` 35 | 36 | ```swift 37 | static func hex(_ hex: String, alpha: CGFloat = 1) -> Self 38 | ``` 39 | 40 | Initialize with HEX 41 | - Parameter hex: HEX String value 42 | - Returns: Color 43 | 44 | #### Parameters 45 | 46 | | Name | Description | 47 | | ---- | ----------- | 48 | | hex | HEX String value | 49 | 50 | ### `init(hex:)` 51 | 52 | ```swift 53 | init(hex: String) 54 | ``` 55 | 56 | Initialize with HEX 57 | - Parameter hex: HEX 58 | 59 | #### Parameters 60 | 61 | | Name | Description | 62 | | ---- | ----------- | 63 | | hex | HEX | 64 | 65 | ### `random(opacity:)` 66 | 67 | ```swift 68 | static func random(opacity: CGFloat = 1.0) -> Color 69 | ``` 70 | 71 | Generate a random color 72 | - Parameter opacity: opacity 73 | - Returns: New color 74 | 75 | #### Parameters 76 | 77 | | Name | Description | 78 | | ---- | ----------- | 79 | | opacity | opacity | 80 | 81 | ### `lighten(by:)` 82 | 83 | ```swift 84 | func lighten(by percentage: CGFloat = 30.0) -> Color 85 | ``` 86 | 87 | Lighten color 88 | - Parameter percentage: percentage (1 -100), default: 30 89 | - Returns: new color 90 | 91 | #### Parameters 92 | 93 | | Name | Description | 94 | | ---- | ----------- | 95 | | percentage | percentage (1 -100), default: 30 | 96 | 97 | ### `darken(by:)` 98 | 99 | ```swift 100 | func darken(by percentage: CGFloat = 30.0) -> Color 101 | ``` 102 | 103 | Darken color 104 | - Parameter percentage: percentage (1 -100), default: 30 105 | - Returns: new color 106 | 107 | #### Parameters 108 | 109 | | Name | Description | 110 | | ---- | ----------- | 111 | | percentage | percentage (1 -100), default: 30 | 112 | 113 | ### `dynamicColor(light:dark:)` 114 | 115 | ```swift 116 | static func dynamicColor(light: UIColor, dark: UIColor) -> Color 117 | ``` 118 | 119 | Create a color with a dark and light mode UIColor 120 | - Parameters: 121 | - light: light color 122 | - dark: dark color 123 | - Returns: Color 124 | 125 | #### Parameters 126 | 127 | | Name | Description | 128 | | ---- | ----------- | 129 | | light | light color | 130 | | dark | dark color | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Data.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Data` 4 | ```swift 5 | public extension Data 6 | ``` 7 | 8 | ## Properties 9 | ### `hexString` 10 | 11 | ```swift 12 | var hexString: String 13 | ``` 14 | 15 | Returns a string of hex value. 16 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Date.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Date` 4 | ```swift 5 | public extension Date 6 | ``` 7 | 8 | ## Properties 9 | ### `year` 10 | 11 | ```swift 12 | var year: Int 13 | ``` 14 | 15 | Returns the year from the date. 16 | 17 | ### `month` 18 | 19 | ```swift 20 | var month: Int 21 | ``` 22 | 23 | Returns month as `Int` starting from `1...12`. 24 | 25 | ### `week` 26 | 27 | ```swift 28 | var week: Int 29 | ``` 30 | 31 | Returns week as `Int` starting from 1...52 32 | 33 | ### `weekday` 34 | 35 | ```swift 36 | var weekday: Int 37 | ``` 38 | 39 | ### `weekOfMonth` 40 | 41 | ```swift 42 | var weekOfMonth: Int 43 | ``` 44 | 45 | ### `day` 46 | 47 | ```swift 48 | var day: Int 49 | ``` 50 | 51 | ### `hour` 52 | 53 | ```swift 54 | var hour: Int 55 | ``` 56 | 57 | ### `minute` 58 | 59 | ```swift 60 | var minute: Int 61 | ``` 62 | 63 | ### `second` 64 | 65 | ```swift 66 | var second: Int 67 | ``` 68 | 69 | ### `nanos` 70 | 71 | ```swift 72 | var nanos: Int 73 | ``` 74 | 75 | ### `yesterday` 76 | 77 | ```swift 78 | var yesterday: Date 79 | ``` 80 | 81 | ### `today` 82 | 83 | ```swift 84 | var today: Date 85 | ``` 86 | 87 | ### `tomorrow` 88 | 89 | ```swift 90 | var tomorrow: Date 91 | ``` 92 | 93 | ### `dayAfter` 94 | 95 | ```swift 96 | var dayAfter: Date 97 | ``` 98 | 99 | ### `dayBefore` 100 | 101 | ```swift 102 | var dayBefore: Date 103 | ``` 104 | 105 | ### `isLastDayOfMonth` 106 | 107 | ```swift 108 | var isLastDayOfMonth: Bool 109 | ``` 110 | 111 | ### `startOfDay` 112 | 113 | ```swift 114 | var startOfDay: Date 115 | ``` 116 | 117 | Start of current day 118 | 119 | ### `endOfDay` 120 | 121 | ```swift 122 | var endOfDay: Date 123 | ``` 124 | 125 | End of current day 126 | 127 | ## Methods 128 | ### `adjust(_:offset:)` 129 | 130 | ```swift 131 | func adjust(_ type: Calendar.Component, offset: Int) -> Date 132 | ``` 133 | 134 | Offset component by amount 135 | - Parameters: 136 | - type: Component 137 | - offset: Offset to add 138 | - Returns: Date 139 | 140 | #### Parameters 141 | 142 | | Name | Description | 143 | | ---- | ----------- | 144 | | type | Component | 145 | | offset | Offset to add | 146 | 147 | ### `toRelativeFormat(currentDate:numericDates:)` 148 | 149 | ```swift 150 | func toRelativeFormat( 151 | currentDate: Date = Date(), 152 | numericDates: Bool = true 153 | ) -> String 154 | ``` 155 | 156 | Get relative String back from Date ex: 1 year ago, 1 month ago ... 157 | - Parameters: 158 | - currentDate: Current Date 159 | - numericDates: Display the numeric value in string ex: 1 year ago vs Last year 160 | - Returns: String 161 | 162 | #### Parameters 163 | 164 | | Name | Description | 165 | | ---- | ----------- | 166 | | currentDate | Current Date | 167 | | numericDates | Display the numeric value in string ex: 1 year ago vs Last year | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/FileManager.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `FileManager` 4 | ```swift 5 | public extension FileManager 6 | ``` 7 | 8 | ## Methods 9 | ### `fileSize(atPath:)` 10 | 11 | ```swift 12 | func fileSize(atPath path: String) -> Int 13 | ``` 14 | 15 | Size of file at path 16 | 17 | - Parameter path: file path 18 | - Returns: Size in bytes 19 | 20 | #### Parameters 21 | 22 | | Name | Description | 23 | | ---- | ----------- | 24 | | path | file path | 25 | 26 | ### `folderSize(atPath:)` 27 | 28 | ```swift 29 | func folderSize(atPath path: String) -> Int 30 | ``` 31 | 32 | Size of folder 33 | 34 | - Parameter path: folder path 35 | - Returns: size in bytes 36 | 37 | #### Parameters 38 | 39 | | Name | Description | 40 | | ---- | ----------- | 41 | | path | folder path | 42 | 43 | ### `directorySize(at:)` 44 | 45 | ```swift 46 | func directorySize(at URL: URL) -> Int 47 | ``` 48 | 49 | Size of directory at URL 50 | 51 | - Parameter URL: URL 52 | - Returns: Size in bytes 53 | 54 | #### Parameters 55 | 56 | | Name | Description | 57 | | ---- | ----------- | 58 | | URL | URL | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/GeometryProxy.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `GeometryProxy` 4 | ```swift 5 | public extension GeometryProxy 6 | ``` 7 | 8 | ## Properties 9 | ### `safeWidth` 10 | 11 | ```swift 12 | var safeWidth: CGFloat 13 | ``` 14 | 15 | Returns the width minus the safeAreaInsets. 16 | 17 | ### `safeHeight` 18 | 19 | ```swift 20 | var safeHeight: CGFloat 21 | ``` 22 | 23 | Returns the height minus the safeAreaInsets. 24 | 25 | ### `localFrame` 26 | 27 | ```swift 28 | var localFrame: CGRect 29 | ``` 30 | 31 | ### `localWidth` 32 | 33 | ```swift 34 | var localWidth: CGFloat 35 | ``` 36 | 37 | ### `localHeight` 38 | 39 | ```swift 40 | var localHeight: CGFloat 41 | ``` 42 | 43 | ### `localCenter` 44 | 45 | ```swift 46 | var localCenter: CGPoint 47 | ``` 48 | 49 | ### `localTop` 50 | 51 | ```swift 52 | var localTop: CGFloat 53 | ``` 54 | 55 | ### `localBottom` 56 | 57 | ```swift 58 | var localBottom: CGFloat 59 | ``` 60 | 61 | ### `localLeft` 62 | 63 | ```swift 64 | var localLeft: CGFloat 65 | ``` 66 | 67 | ### `localRight` 68 | 69 | ```swift 70 | var localRight: CGFloat 71 | ``` 72 | 73 | ### `localDiameter` 74 | 75 | ```swift 76 | var localDiameter: CGFloat 77 | ``` 78 | 79 | ### `globalFrame` 80 | 81 | ```swift 82 | var globalFrame: CGRect 83 | ``` 84 | 85 | ### `globalWidth` 86 | 87 | ```swift 88 | var globalWidth: CGFloat 89 | ``` 90 | 91 | ### `globalHeight` 92 | 93 | ```swift 94 | var globalHeight: CGFloat 95 | ``` 96 | 97 | ### `globalCenter` 98 | 99 | ```swift 100 | var globalCenter: CGPoint 101 | ``` 102 | 103 | ### `globalTop` 104 | 105 | ```swift 106 | var globalTop: CGFloat 107 | ``` 108 | 109 | ### `globalBottom` 110 | 111 | ```swift 112 | var globalBottom: CGFloat 113 | ``` 114 | 115 | ### `globalLeft` 116 | 117 | ```swift 118 | var globalLeft: CGFloat 119 | ``` 120 | 121 | ### `globalRight` 122 | 123 | ```swift 124 | var globalRight: CGFloat 125 | ``` 126 | 127 | ### `globalDiameter` 128 | 129 | ```swift 130 | var globalDiameter: CGFloat 131 | ``` 132 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Image.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Image` 4 | ```swift 5 | public extension Image 6 | ``` 7 | 8 | ## Methods 9 | ### `init(_:defaultImage:)` 10 | 11 | ```swift 12 | init(_ name: String, defaultImage: String) 13 | ``` 14 | 15 | Initialize with a default image 16 | - Parameters: 17 | - name: primary image name 18 | - defaultImage: default image name 19 | 20 | #### Parameters 21 | 22 | | Name | Description | 23 | | ---- | ----------- | 24 | | name | primary image name | 25 | | defaultImage | default image name | 26 | 27 | ### `init(_:defaultSystemImage:)` 28 | 29 | ```swift 30 | init(_ name: String, defaultSystemImage: String) 31 | ``` 32 | 33 | Initialize with default system image 34 | - Parameters: 35 | - name: primary image name 36 | - defaultSystemImage: default image name 37 | 38 | #### Parameters 39 | 40 | | Name | Description | 41 | | ---- | ----------- | 42 | | name | primary image name | 43 | | defaultSystemImage | default image name | 44 | 45 | ### `icon(with:)` 46 | 47 | ```swift 48 | func icon(with size: CGSize) -> some View 49 | ``` 50 | 51 | Create a resizable image with CGSize 52 | - Parameter size: CGSize 53 | - Returns: View 54 | 55 | #### Parameters 56 | 57 | | Name | Description | 58 | | ---- | ----------- | 59 | | size | CGSize | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Int.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Int` 4 | ```swift 5 | public extension Int 6 | ``` 7 | 8 | ## Properties 9 | ### `isOdd` 10 | 11 | ```swift 12 | var isOdd: Bool 13 | ``` 14 | 15 | Whether self is an odd number 16 | 17 | ### `isEven` 18 | 19 | ```swift 20 | var isEven: Bool 21 | ``` 22 | 23 | Whether self is an even number 24 | 25 | ### `nilIfZero` 26 | 27 | ```swift 28 | var nilIfZero: Int? 29 | ``` 30 | 31 | Treats 0 as nil 32 | 33 | ### `string` 34 | 35 | ```swift 36 | var string: String 37 | ``` 38 | 39 | Make the number to string 40 | 41 | ### `range` 42 | 43 | ```swift 44 | var range: CountableRange 45 | ``` 46 | 47 | Make a range from zero to self 48 | 49 | ### `hours` 50 | 51 | ```swift 52 | var hours: Int 53 | ``` 54 | 55 | ### `minutes` 56 | 57 | ```swift 58 | var minutes: Int 59 | ``` 60 | 61 | ### `days` 62 | 63 | ```swift 64 | var days: Int 65 | ``` 66 | 67 | ### `months` 68 | 69 | ```swift 70 | var months: Int 71 | ``` 72 | 73 | ### `years` 74 | 75 | ```swift 76 | var years: Int 77 | ``` 78 | 79 | ## Methods 80 | ### `instances(of:)` 81 | 82 | ```swift 83 | func instances(of creation: @autoclosure () throws -> T) rethrows -> [T] 84 | ``` 85 | 86 | Return a number of instances 87 | 88 | - Parameter creation: The initialization of the object 89 | - Returns: An array containing the objects 90 | 91 | #### Parameters 92 | 93 | | Name | Description | 94 | | ---- | ----------- | 95 | | creation | The initialization of the object | 96 | 97 | ### `inRange(_:)` 98 | 99 | ```swift 100 | func inRange(_ range: Range) -> Bool 101 | ``` 102 | 103 | Return if `self` is in the given range. 104 | 105 | - Parameter range: Target range. 106 | - Returns: `true` if self is in the range, otherwise `false`. 107 | 108 | #### Parameters 109 | 110 | | Name | Description | 111 | | ---- | ----------- | 112 | | range | Target range. | 113 | 114 | ### `times(block:)` 115 | 116 | ```swift 117 | func times(block: () -> Void) 118 | ``` 119 | 120 | Calls the given block n number of times. 121 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/NSObject.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `NSObject` 4 | ```swift 5 | public extension NSObject 6 | ``` 7 | 8 | ## Properties 9 | ### `className` 10 | 11 | ```swift 12 | var className: String 13 | ``` 14 | 15 | Return class name. 16 | 17 | ## Methods 18 | ### `exchangeImplementations(originalSelector:swizzledSelector:)` 19 | 20 | ```swift 21 | class func exchangeImplementations(originalSelector: Selector, swizzledSelector: Selector) 22 | ``` 23 | 24 | Exchange two implementations of the given selectors, aka method swizzling. 25 | 26 | - Parameters: 27 | - originalSelector: The original selector. 28 | - swizzledSelector: Another selector. 29 | 30 | #### Parameters 31 | 32 | | Name | Description | 33 | | ---- | ----------- | 34 | | originalSelector | The original selector. | 35 | | swizzledSelector | Another selector. | 36 | 37 | ### `printDeinitMessage()` 38 | 39 | ```swift 40 | final func printDeinitMessage() 41 | ``` 42 | 43 | Print the deinitialization message of self. 44 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Publisher.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Publisher` 4 | ```swift 5 | public extension Publisher 6 | ``` 7 | 8 | ## Methods 9 | ### `debug(prefix:function:line:)` 10 | 11 | ```swift 12 | func debug( 13 | prefix: String = "", 14 | function: String = #function, 15 | line: Int = #line) -> Publishers.HandleEvents 16 | ``` 17 | 18 | Debug print Publisher events 19 | - Parameters: 20 | - prefix: Prefix on print statement 21 | - function: Function name 22 | - line: Line number 23 | - Returns: Publisher 24 | 25 | #### Parameters 26 | 27 | | Name | Description | 28 | | ---- | ----------- | 29 | | prefix | Prefix on print statement | 30 | | function | Function name | 31 | | line | Line number | 32 | 33 | ### `receiveOnMain()` 34 | 35 | ```swift 36 | func receiveOnMain() -> Publishers.ReceiveOn 37 | ``` 38 | 39 | Receive Output value on main thread (DispatchQueue.main) 40 | 41 | ### `sink(result:)` 42 | 43 | ```swift 44 | func sink(result: @escaping ((Result) -> Void)) -> AnyCancellable 45 | ``` 46 | 47 | A single value sink function outputs a `Result` 48 | - Parameter result: Result 49 | - Returns: AnyCancellable 50 | 51 | #### Parameters 52 | 53 | | Name | Description | 54 | | ---- | ----------- | 55 | | result | Result | 56 | 57 | ### `assign(to:on:)` 58 | 59 | ```swift 60 | func assign(to keyPath: ReferenceWritableKeyPath, on root: Root) -> AnyCancellable 61 | ``` 62 | 63 | Assign property to object without using [weak self] 64 | - Parameters: 65 | - keyPath: Property keypath 66 | - root: Any object 67 | - Returns: AnyCancellable 68 | 69 | #### Parameters 70 | 71 | | Name | Description | 72 | | ---- | ----------- | 73 | | keyPath | Property keypath | 74 | | root | Any object | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Publishers.Autoconnect.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Publishers.Autoconnect` 4 | ```swift 5 | extension Publishers.Autoconnect: TimerStartable where Upstream: Timer.TimerPublisher 6 | ``` 7 | 8 | ## Methods 9 | ### `stop()` 10 | 11 | ```swift 12 | public func stop() 13 | ``` 14 | 15 | Stops timer of current timer publisher instance 16 | 17 | ### `start(totalTime:)` 18 | 19 | ```swift 20 | public func start(totalTime: TimeInterval? = nil) -> AnyPublisher 21 | ``` 22 | 23 | Stops timer of current timer publisher instance after totalTime 24 | If totalTime is nil, then continues until hard stopped 25 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/RandomAccessCollection.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `RandomAccessCollection` 4 | ```swift 5 | public extension RandomAccessCollection 6 | ``` 7 | 8 | ## Methods 9 | ### `indexed()` 10 | 11 | ```swift 12 | func indexed() -> Array<(offset: Int, element: Element)> 13 | ``` 14 | 15 | Enumerated Array of index, element pair 16 | - Returns: Array 17 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Shape.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Shape` 4 | ```swift 5 | public extension Shape 6 | ``` 7 | 8 | ## Methods 9 | ### `gradientFill(colors:start:end:)` 10 | 11 | ```swift 12 | func gradientFill( 13 | colors: Color..., 14 | start: UnitPoint = .top, 15 | end: UnitPoint = .bottom) -> some View 16 | ``` 17 | 18 | Addd a `LinearGradient` fill on Shape 19 | - Parameters: 20 | - colors: Colors 21 | - start: Start, default top 22 | - end: End, default bottom 23 | - Returns: View 24 | 25 | #### Parameters 26 | 27 | | Name | Description | 28 | | ---- | ----------- | 29 | | colors | Colors | 30 | | start | Start, default top | 31 | | end | End, default bottom | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/String.Index.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `String.Index` 4 | ```swift 5 | public extension String.Index 6 | ``` 7 | 8 | ## Methods 9 | ### `distance(in:)` 10 | 11 | ```swift 12 | func distance(in string: S) -> Int 13 | ``` 14 | 15 | Distance to String value 16 | - Parameter string: String distance too 17 | - Returns: Int 18 | 19 | #### Parameters 20 | 21 | | Name | Description | 22 | | ---- | ----------- | 23 | | string | String distance too | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/String.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `String` 4 | ```swift 5 | public extension String 6 | ``` 7 | 8 | ## Properties 9 | ### `removeHTML` 10 | 11 | ```swift 12 | var removeHTML: String 13 | ``` 14 | 15 | Remove HTML tags from String 16 | 17 | ### `isEmail` 18 | 19 | ```swift 20 | var isEmail: Bool 21 | ``` 22 | 23 | Is valid email 24 | 25 | ### `areNumbers` 26 | 27 | ```swift 28 | var areNumbers: Bool 29 | ``` 30 | 31 | Are numbers 0-9 32 | 33 | ### `toInt` 34 | 35 | ```swift 36 | var toInt: Int? 37 | ``` 38 | 39 | Cast to Int 40 | 41 | ### `toDouble` 42 | 43 | ```swift 44 | var toDouble: Double? 45 | ``` 46 | 47 | Cast to Double 48 | 49 | ### `trimmingZeroDecimal` 50 | 51 | ```swift 52 | var trimmingZeroDecimal: String 53 | ``` 54 | 55 | Trimming ".0" 56 | 57 | ### `addingPlusSymbol` 58 | 59 | ```swift 60 | var addingPlusSymbol: String 61 | ``` 62 | 63 | Adding "+" at the very beginning. 64 | 65 | ### `addingMinusSymbol` 66 | 67 | ```swift 68 | var addingMinusSymbol: String 69 | ``` 70 | 71 | Adding "-" at the very beginning. 72 | 73 | ### `uppercasingFirstLetter` 74 | 75 | ```swift 76 | var uppercasingFirstLetter: String 77 | ``` 78 | 79 | ### `lowercasingFirstLetter` 80 | 81 | ```swift 82 | var lowercasingFirstLetter: String 83 | ``` 84 | 85 | ### `isBlank` 86 | 87 | ```swift 88 | var isBlank: Bool 89 | ``` 90 | 91 | Return `true` if self is empty or only contains white spaces and/or new lines. 92 | 93 | ### `isVisible` 94 | 95 | ```swift 96 | var isVisible: Bool 97 | ``` 98 | 99 | Return `false` if self is empty or only contains white spaces and/or new lines. 100 | 101 | ### `nilIfBlank` 102 | 103 | ```swift 104 | var nilIfBlank: String? 105 | ``` 106 | 107 | Return `nil` if `self.isBlank` is `true`. 108 | 109 | ## Methods 110 | ### `trim()` 111 | 112 | ```swift 113 | func trim() -> String 114 | ``` 115 | 116 | Trim white space and new lines 117 | 118 | ### `layoutSize(with:)` 119 | 120 | ```swift 121 | func layoutSize(with font: UIFont) -> CGSize 122 | ``` 123 | 124 | Returns the CGSize that the string being layout on screen. 125 | 126 | - Parameter font: The given font. 127 | - Returns: The result CGSize. 128 | 129 | #### Parameters 130 | 131 | | Name | Description | 132 | | ---- | ----------- | 133 | | font | The given font. | 134 | 135 | ### `advanceNumberValue(step:)` 136 | 137 | ```swift 138 | mutating func advanceNumberValue(step: Int = 1) 139 | ``` 140 | 141 | Cast as Int and add the given value. No changes if casting fails. 142 | 143 | ### `isOldAppVersion(comparedWith:)` 144 | 145 | ```swift 146 | func isOldAppVersion(comparedWith aVersion: String) -> Bool 147 | ``` 148 | 149 | Comparing app versions. Returns `true` if self is `1.1.0` and the given value is `1.2.0`. 150 | - Parameter aVersion: Another version. 151 | - Returns: `true` if the give version is newer than self. 152 | 153 | #### Parameters 154 | 155 | | Name | Description | 156 | | ---- | ----------- | 157 | | aVersion | Another version. | 158 | 159 | ### `treatsVisuallyEmptyAsNil()` 160 | 161 | ```swift 162 | func treatsVisuallyEmptyAsNil() -> String? 163 | ``` 164 | 165 | ### `slice(from:to:)` 166 | 167 | ```swift 168 | func slice(from: String, to: String) -> String? 169 | ``` 170 | 171 | Get subscring from created range 172 | - Parameters: 173 | - from: from String 174 | - to: to String 175 | - Returns: String 176 | 177 | #### Parameters 178 | 179 | | Name | Description | 180 | | ---- | ----------- | 181 | | from | from String | 182 | | to | to String | 183 | 184 | ### `copyToPasteboard()` 185 | 186 | ```swift 187 | func copyToPasteboard() 188 | ``` 189 | 190 | Copy self to UIPasteboard 191 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Text.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Text` 4 | ```swift 5 | public extension Text 6 | ``` 7 | 8 | ## Methods 9 | ### `init(_:dateStyle:)` 10 | 11 | ```swift 12 | init(_ date: Date, dateStyle: DateFormatter.Style = .full) 13 | ``` 14 | 15 | Create Text from date formate 16 | - Parameters: 17 | - date: `Date` 18 | - dateStyle: `DateFormatter.Style`, default .full 19 | 20 | #### Parameters 21 | 22 | | Name | Description | 23 | | ---- | ----------- | 24 | | date | `Date` | 25 | | dateStyle | `DateFormatter.Style`, default .full | 26 | 27 | ### `init(_:formatter:)` 28 | 29 | ```swift 30 | init(_ date: Date, formatter: DateFormatter = Text.dateFormatter) 31 | ``` 32 | 33 | Create Text with date formatter string 34 | - Parameters: 35 | - date: `Date` 36 | - formatter: `DateFormatter` 37 | 38 | #### Parameters 39 | 40 | | Name | Description | 41 | | ---- | ----------- | 42 | | date | `Date` | 43 | | formatter | `DateFormatter` | 44 | 45 | ### `system(_:weight:design:)` 46 | 47 | ```swift 48 | func system(_ size: CGFloat = 18, weight: SwiftUI.Font.Weight = .regular, design: SwiftUI.Font.Design = .default) -> Text 49 | ``` 50 | 51 | Text with system font 52 | - Parameters: 53 | - size: size, default 18 54 | - weight: font weight, default regular 55 | - design: font design, default .default 56 | - Returns: View 57 | 58 | #### Parameters 59 | 60 | | Name | Description | 61 | | ---- | ----------- | 62 | | size | size, default 18 | 63 | | weight | font weight, default regular | 64 | | design | font design, default .default | 65 | 66 | ### `monospaced(_:weight:design:)` 67 | 68 | ```swift 69 | func monospaced(_ size: CGFloat = 18, weight: SwiftUI.Font.Weight = .regular, design: SwiftUI.Font.Design = .monospaced) -> Text 70 | ``` 71 | 72 | Text with system font and monospaced 73 | - Parameters: 74 | - size: size, default 18 75 | - weight: font weight, default regular 76 | - design: font design, default .monospaced 77 | - Returns: View 78 | 79 | #### Parameters 80 | 81 | | Name | Description | 82 | | ---- | ----------- | 83 | | size | size, default 18 | 84 | | weight | font weight, default regular | 85 | | design | font design, default .monospaced | 86 | 87 | ### `templateSize(for:)` 88 | 89 | ```swift 90 | func templateSize(for value: String) -> some View 91 | ``` 92 | 93 | Hidden `View` with frame of String vale 94 | - Parameter value: String 95 | - Returns: View 96 | 97 | #### Parameters 98 | 99 | | Name | Description | 100 | | ---- | ----------- | 101 | | value | String | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/Timer.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `Timer` 4 | ```swift 5 | public extension Timer 6 | ``` 7 | 8 | ## Methods 9 | ### `schedule(delay:handler:)` 10 | 11 | ```swift 12 | class func schedule(delay: TimeInterval, handler: @escaping () -> Void) -> CFRunLoopTimer? 13 | ``` 14 | 15 | Schedule closure to run on main run loop after delay 16 | 17 | - Parameters: 18 | - delay: Delay interval 19 | - handler: Closure to run 20 | - Returns: `CFRunLoopTimer` 21 | 22 | #### Parameters 23 | 24 | | Name | Description | 25 | | ---- | ----------- | 26 | | delay | Delay interval | 27 | | handler | Closure to run | 28 | 29 | ### `schedule(repeatInterval:handler:)` 30 | 31 | ```swift 32 | class func schedule(repeatInterval interval: TimeInterval, handler: @escaping () -> Void) -> CFRunLoopTimer? 33 | ``` 34 | 35 | Schedule closure to run on main run loop and repeat at the interval 36 | 37 | - Parameters: 38 | - interval: Interval 39 | - handler: Closure to run 40 | - Returns: CFRunLoopTimer 41 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/UIApplication.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `UIApplication` 4 | ```swift 5 | public extension UIApplication 6 | ``` 7 | 8 | ## Properties 9 | ### `keyWindow` 10 | 11 | ```swift 12 | var keyWindow: UIWindow? 13 | ``` 14 | 15 | Application key window 16 | 17 | ### `isConnectedToNetwork` 18 | 19 | ```swift 20 | var isConnectedToNetwork: Bool 21 | ``` 22 | 23 | Checks if the application is connected to the internet via `SystemConfiguration` 24 | 25 | ## Methods 26 | ### `openSettings()` 27 | 28 | ```swift 29 | func openSettings() 30 | ``` 31 | 32 | Open application settings 33 | 34 | ### `openPhone(calling:)` 35 | 36 | ```swift 37 | func openPhone(calling number: String) 38 | ``` 39 | 40 | Opens application sheet for phonen number 41 | 42 | ### `openFacebook(name:id:)` 43 | 44 | ```swift 45 | func openFacebook(name: String?, id: String?) 46 | ``` 47 | 48 | Find My Facebook ID: https://findmyfbid.com/ 49 | - Parameters: 50 | - name: Facebook name 51 | - id: Facebook ID 52 | 53 | #### Parameters 54 | 55 | | Name | Description | 56 | | ---- | ----------- | 57 | | name | Facebook name | 58 | | id | Facebook ID | 59 | 60 | ### `openInstagram(name:)` 61 | 62 | ```swift 63 | func openInstagram(name: String?) 64 | ``` 65 | 66 | Launches the Instagram app and loads the Instagram user 67 | - Parameter name: Instagram username 68 | 69 | #### Parameters 70 | 71 | | Name | Description | 72 | | ---- | ----------- | 73 | | name | Instagram username | 74 | 75 | ### `openInstagram(media:)` 76 | 77 | ```swift 78 | func openInstagram(media: String?) 79 | ``` 80 | 81 | Launches the Instagram app and loads the Instagram user 82 | - Parameter name: Instagram username 83 | 84 | #### Parameters 85 | 86 | | Name | Description | 87 | | ---- | ----------- | 88 | | name | Instagram username | 89 | 90 | ### `openExternalMapApp(query:)` 91 | 92 | ```swift 93 | func openExternalMapApp(query: String) 94 | ``` 95 | 96 | Open a map app with the given query. Orders: Google Map -> Apple Map 97 | 98 | - Parameter query: The query to search on the map. 99 | 100 | #### Parameters 101 | 102 | | Name | Description | 103 | | ---- | ----------- | 104 | | query | The query to search on the map. | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/UIButton.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `UIButton` 4 | ```swift 5 | public extension UIButton 6 | ``` 7 | 8 | ## Properties 9 | ### `normalStateBackgroundColor` 10 | 11 | ```swift 12 | @IBInspectable var normalStateBackgroundColor: UIColor? 13 | ``` 14 | 15 | ### `disabledStateBackgroundColor` 16 | 17 | ```swift 18 | @IBInspectable var disabledStateBackgroundColor: UIColor? 19 | ``` 20 | 21 | ### `highlightedStateBackgroundColor` 22 | 23 | ```swift 24 | @IBInspectable var highlightedStateBackgroundColor: UIColor? 25 | ``` 26 | 27 | ### `selectedStateBackgroundColor` 28 | 29 | ```swift 30 | @IBInspectable var selectedStateBackgroundColor: UIColor? 31 | ``` 32 | 33 | ### `titleImageSpacing` 34 | 35 | ```swift 36 | @IBInspectable var titleImageSpacing: CGFloat 37 | ``` 38 | 39 | ### `isTitleImagePositionReversed` 40 | 41 | ```swift 42 | var isTitleImagePositionReversed: Bool 43 | ``` 44 | 45 | ### `backgroundImageView` 46 | 47 | ```swift 48 | var backgroundImageView: UIImageView? 49 | ``` 50 | 51 | ## Methods 52 | ### `setBackgroundColor(_:for:)` 53 | 54 | ```swift 55 | func setBackgroundColor(_ color: UIColor, for state: UIControl.State) 56 | ``` 57 | 58 | Set background color for state 59 | - Parameters: 60 | - color: color 61 | - state: state 62 | 63 | #### Parameters 64 | 65 | | Name | Description | 66 | | ---- | ----------- | 67 | | color | color | 68 | | state | state | 69 | 70 | ### `centerTextAndImage(spacing:forceRightToLeft:)` 71 | 72 | ```swift 73 | func centerTextAndImage(spacing: CGFloat, forceRightToLeft: Bool) 74 | ``` 75 | 76 | Adjust `contentEdgeInsets`, `imageEdgeInsets` and `titleEdgeInsets` with appropriate value so as to make a specified spacing between the button's title and image. 77 | - Reference: https://stackoverflow.com/questions/4564621/aligning-text-and-image-on-uibutton-with-imageedgeinsets-and-titleedgeinsets 78 | 79 | - Parameters: 80 | - spacing: The desired spacing to make. 81 | - forceRightToLeft: Whether the content of the button is in `forceRightToLeft` semantic. 82 | 83 | #### Parameters 84 | 85 | | Name | Description | 86 | | ---- | ----------- | 87 | | spacing | The desired spacing to make. | 88 | | forceRightToLeft | Whether the content of the button is in `forceRightToLeft` semantic. | 89 | 90 | ### `addAction(for:closure:)` 91 | 92 | ```swift 93 | func addAction(for controlEvent: UIControl.Event, closure: @escaping () -> Void) 94 | ``` 95 | 96 | Button action for event 97 | - Parameters: 98 | - controlEvent: Event 99 | - closure: Closure to run 100 | 101 | #### Parameters 102 | 103 | | Name | Description | 104 | | ---- | ----------- | 105 | | controlEvent | Event | 106 | | closure | Closure to run | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/UIColor.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `UIColor` 4 | ```swift 5 | public extension UIColor 6 | ``` 7 | 8 | ## Properties 9 | ### `isLight` 10 | 11 | ```swift 12 | var isLight: Bool 13 | ``` 14 | 15 | Check whether self is a light/bright color. 16 | https://stackoverflow.com/questions/2509443/check-if-uicolor-is-dark-or-bright 17 | 18 | ### `isExtremelyLight` 19 | 20 | ```swift 21 | var isExtremelyLight: Bool 22 | ``` 23 | 24 | Check whether self is a light/bright color. 25 | https://stackoverflow.com/questions/2509443/check-if-uicolor-is-dark-or-bright 26 | 27 | ## Methods 28 | ### `init(rgbValue:alpha:)` 29 | 30 | ```swift 31 | convenience init(rgbValue: UInt, alpha: CGFloat = 1) 32 | ``` 33 | 34 | New color from RGB value 35 | - Parameters: 36 | - rgbValue: value 37 | - alpha: alpha 38 | 39 | #### Parameters 40 | 41 | | Name | Description | 42 | | ---- | ----------- | 43 | | rgbValue | value | 44 | | alpha | alpha | 45 | 46 | ### `init(hexCode:alpha:)` 47 | 48 | ```swift 49 | convenience init(hexCode: String, alpha: CGFloat = 1) 50 | ``` 51 | 52 | Color from HEX 53 | - Parameter hexCode: Hex w/o `#` 54 | 55 | #### Parameters 56 | 57 | | Name | Description | 58 | | ---- | ----------- | 59 | | hexCode | Hex w/o `#` | 60 | 61 | ### `random(alpha:)` 62 | 63 | ```swift 64 | static func random(alpha: CGFloat = 1.0) -> UIColor 65 | ``` 66 | 67 | Random color 68 | - Parameter alpha: alpha 69 | - Returns: new color 70 | 71 | #### Parameters 72 | 73 | | Name | Description | 74 | | ---- | ----------- | 75 | | alpha | alpha | 76 | 77 | ### `lighten(by:)` 78 | 79 | ```swift 80 | func lighten(by percentage: CGFloat = 30.0) -> UIColor 81 | ``` 82 | 83 | Darken color 84 | - Parameter percentage: percentage 85 | - Returns: new color 86 | 87 | #### Parameters 88 | 89 | | Name | Description | 90 | | ---- | ----------- | 91 | | percentage | percentage | 92 | 93 | ### `darken(by:)` 94 | 95 | ```swift 96 | func darken(by percentage: CGFloat = 30.0) -> UIColor 97 | ``` 98 | 99 | Darken color 100 | - Parameter percentage: percentage 101 | - Returns: new color 102 | 103 | #### Parameters 104 | 105 | | Name | Description | 106 | | ---- | ----------- | 107 | | percentage | percentage | 108 | 109 | ### `dynamicColor(light:dark:)` 110 | 111 | ```swift 112 | static func dynamicColor(light: UIColor, dark: UIColor) -> UIColor 113 | ``` 114 | 115 | Color for UI appearance ex: dark/light mode 116 | - Parameters: 117 | - light: Light Color 118 | - dark: Dark Color 119 | - Returns: UIColor 120 | 121 | #### Parameters 122 | 123 | | Name | Description | 124 | | ---- | ----------- | 125 | | light | Light Color | 126 | | dark | Dark Color | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/UITableView.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `UITableView` 4 | ```swift 5 | public extension UITableView 6 | ``` 7 | 8 | ## Properties 9 | ### `isEmpty` 10 | 11 | ```swift 12 | var isEmpty: Bool 13 | ``` 14 | 15 | Zero rows in all sections 16 | 17 | ## Methods 18 | ### `reloadCell(_:with:)` 19 | 20 | ```swift 21 | func reloadCell(_ cell: UITableViewCell, with animation: UITableView.RowAnimation) 22 | ``` 23 | 24 | Reload cell at index path 25 | - Parameters: 26 | - cell: cell 27 | - animation: animation 28 | 29 | #### Parameters 30 | 31 | | Name | Description | 32 | | ---- | ----------- | 33 | | cell | cell | 34 | | animation | animation | 35 | 36 | ### `scrollToBottom(position:animated:)` 37 | 38 | ```swift 39 | func scrollToBottom(position: UITableView.ScrollPosition = .bottom, animated: Bool = true) 40 | ``` 41 | 42 | Scroll to last index in last section 43 | - Parameters: 44 | - position: position 45 | - animated: animated 46 | 47 | #### Parameters 48 | 49 | | Name | Description | 50 | | ---- | ----------- | 51 | | position | position | 52 | | animated | animated | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/UIView.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `UIView` 4 | ```swift 5 | public extension UIView 6 | ``` 7 | 8 | ## Properties 9 | ### `anchorPoint` 10 | 11 | ```swift 12 | var anchorPoint: CGPoint 13 | ``` 14 | 15 | Set anchor point on layer 16 | 17 | ### `layerCornerRadius` 18 | 19 | ```swift 20 | @IBInspectable var layerCornerRadius: CGFloat 21 | ``` 22 | 23 | Set corner radius on layer 24 | 25 | ### `layerBorderWidth` 26 | 27 | ```swift 28 | @IBInspectable var layerBorderWidth: CGFloat 29 | ``` 30 | 31 | Set border width on layer 32 | 33 | ### `layerBorderColor` 34 | 35 | ```swift 36 | @IBInspectable var layerBorderColor: UIColor? 37 | ``` 38 | 39 | Set a border color on layer 40 | 41 | ## Methods 42 | ### `addCleanBorder(cornerRadius:color:width:targetBounds:)` 43 | 44 | ```swift 45 | func addCleanBorder(cornerRadius: CGFloat, color: UIColor, width: CGFloat, targetBounds: CGRect?) 46 | ``` 47 | 48 | Add border using path 49 | - Parameters: 50 | - cornerRadius: radius 51 | - color: color 52 | - width: width 53 | - targetBounds: bounds or view bounds 54 | 55 | #### Parameters 56 | 57 | | Name | Description | 58 | | ---- | ----------- | 59 | | cornerRadius | radius | 60 | | color | color | 61 | | width | width | 62 | | targetBounds | bounds or view bounds | 63 | 64 | ### `addCleanBorder(roundingCorners:cornerRadii:color:width:targetBounds:)` 65 | 66 | ```swift 67 | func addCleanBorder(roundingCorners: UIRectCorner, cornerRadii: CGSize, color: UIColor, width: CGFloat, targetBounds: CGRect?) 68 | ``` 69 | 70 | Add border using path 71 | - Parameters: 72 | - roundingCorners: Corners to round 73 | - cornerRadii: radius 74 | - color: color 75 | - width: width 76 | - targetBounds: view bounds 77 | 78 | #### Parameters 79 | 80 | | Name | Description | 81 | | ---- | ----------- | 82 | | roundingCorners | Corners to round | 83 | | cornerRadii | radius | 84 | | color | color | 85 | | width | width | 86 | | targetBounds | view bounds | 87 | 88 | ### `addTransparentGradientLayer(frame:toColor:minAlpha:maxAlpha:from:to:)` 89 | 90 | ```swift 91 | func addTransparentGradientLayer(frame: CGRect? = nil, toColor: UIColor, minAlpha: CGFloat, maxAlpha: CGFloat, from start: GradientLayerChangingDirection, to end: GradientLayerChangingDirection) -> CAGradientLayer 92 | ``` 93 | 94 | Add a transparent gradient layer 95 | 96 | - Parameters: 97 | - frame: View frame (default to bounds) 98 | - toColor: Color to add 99 | - minAlpha: Alpha 100 | - maxAlpha: End alpha 101 | - start: Start direction 102 | - end: End direction 103 | 104 | #### Parameters 105 | 106 | | Name | Description | 107 | | ---- | ----------- | 108 | | frame | View frame (default to bounds) | 109 | | toColor | Color to add | 110 | | minAlpha | Alpha | 111 | | maxAlpha | End alpha | 112 | | start | Start direction | 113 | | end | End direction | 114 | 115 | ### `sizeToFitConstraintedBasedLayout()` 116 | 117 | ```swift 118 | func sizeToFitConstraintedBasedLayout() 119 | ``` 120 | 121 | Resizes `self.frame` so as to fit the constraint-based layout. 122 | 123 | ### `shakeToIndicateError()` 124 | 125 | ```swift 126 | func shakeToIndicateError() 127 | ``` 128 | 129 | Commit a shake animation and vibrate to indicate the occured error. 130 | 131 | ### `dropRealShadow(shadowColor:fillColor:opacity:offset:radius:)` 132 | 133 | ```swift 134 | func dropRealShadow(shadowColor: UIColor = UIColor.black, fillColor: UIColor = UIColor.white, opacity: Float = 0.2, offset: CGSize = CGSize(width: 0.0, height: 1.0), radius: CGFloat = 10) -> CAShapeLayer 135 | ``` 136 | 137 | New drop shadow by adding new layer 138 | - Parameters: 139 | - shadowColor: color 140 | - fillColor: fill default: white 141 | - opacity: opacity default 20% 142 | - offset: offset default y: 1 143 | - radius: radius (blur) default 10 144 | - Returns: new layer 145 | 146 | #### Parameters 147 | 148 | | Name | Description | 149 | | ---- | ----------- | 150 | | shadowColor | color | 151 | | fillColor | fill default: white | 152 | | opacity | opacity default 20% | 153 | | offset | offset default y: 1 | 154 | | radius | radius (blur) default 10 | 155 | 156 | ### `dropShadow(color:opacity:offSet:radius:scale:)` 157 | 158 | ```swift 159 | func dropShadow(color: UIColor, opacity: Float = 0.5, offSet: CGSize, radius: CGFloat = 1, scale: Bool = true) 160 | ``` 161 | 162 | Draw a simple drop shadow for the views layer 163 | - Parameters: 164 | - color: color 165 | - opacity: opacity default: 50% 166 | - offSet: offset 167 | - radius: radius (blur) default 1 168 | - scale: scale default: true 169 | 170 | #### Parameters 171 | 172 | | Name | Description | 173 | | ---- | ----------- | 174 | | color | color | 175 | | opacity | opacity default: 50% | 176 | | offSet | offset | 177 | | radius | radius (blur) default 1 | 178 | | scale | scale default: true | 179 | 180 | ### `fillParent(view:with:)` 181 | 182 | ```swift 183 | func fillParent(view: UIView, with insets: UIEdgeInsets = .zero) 184 | ``` 185 | 186 | Constrain view to parent view 187 | - Parameters: 188 | - view: Parent view 189 | - insets: Margin insets 190 | 191 | #### Parameters 192 | 193 | | Name | Description | 194 | | ---- | ----------- | 195 | | view | Parent view | 196 | | insets | Margin insets | 197 | 198 | ### `clear()` 199 | 200 | ```swift 201 | static func clear() -> UIView 202 | ``` 203 | 204 | Create a view with a clear background 205 | - Returns: UIView 206 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/URL.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `URL` 4 | ```swift 5 | public extension URL 6 | ``` 7 | 8 | ## Methods 9 | ### `init(_:)` 10 | 11 | ```swift 12 | init(_ string: StaticString) 13 | ``` 14 | 15 | Create URL from String 16 | - Parameter string: URL string 17 | 18 | #### Parameters 19 | 20 | | Name | Description | 21 | | ---- | ----------- | 22 | | string | URL string | 23 | 24 | ### `init(_:path:parameters:)` 25 | 26 | ```swift 27 | init?( 28 | _ baseUrl: StaticString, 29 | path: StaticString = "", 30 | parameters: [String: T] = [:] 31 | ) 32 | ``` 33 | 34 | Create URL from baseUrl, path, and parameters 35 | - Parameters: 36 | - baseUrl: base URL including the host (https) 37 | - path: url path default, empty string 38 | - parameters: parameters, key/value pair 39 | 40 | #### Parameters 41 | 42 | | Name | Description | 43 | | ---- | ----------- | 44 | | baseUrl | base URL including the host (https) | 45 | | path | url path default, empty string | 46 | | parameters | parameters, key/value pair | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/extensions/UserDefaults.md: -------------------------------------------------------------------------------- 1 | **EXTENSION** 2 | 3 | # `UserDefaults` 4 | ```swift 5 | public extension UserDefaults 6 | ``` 7 | 8 | ## Methods 9 | ### `getCodable(forKey:)` 10 | 11 | ```swift 12 | func getCodable(forKey key: String) -> T? 13 | ``` 14 | 15 | Get `Codable` model from user defaults 16 | - Parameter key: String key 17 | - Returns: Model 18 | 19 | #### Parameters 20 | 21 | | Name | Description | 22 | | ---- | ----------- | 23 | | key | String key | 24 | 25 | ### `setCodable(value:forKey:)` 26 | 27 | ```swift 28 | func setCodable(value: T, forKey key: String) 29 | ``` 30 | 31 | Set `Codable` mode to user defaults 32 | - Parameters: 33 | - value: Model 34 | - key: String key 35 | 36 | #### Parameters 37 | 38 | | Name | Description | 39 | | ---- | ----------- | 40 | | value | Model | 41 | | key | String key | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/methods/+(____).md: -------------------------------------------------------------------------------- 1 | ### `+(_:_:)` 2 | 3 | ```swift 4 | public func +(lhs: CGSize, rhs: CGSize) -> CGSize 5 | ``` 6 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/methods/-(____).md: -------------------------------------------------------------------------------- 1 | ### `-(_:_:)` 2 | 3 | ```swift 4 | public func -(lhs: CGSize, rhs: CGSize) -> CGSize 5 | ``` 6 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/methods/_(____).md: -------------------------------------------------------------------------------- 1 | ### `/(_:_:)` 2 | 3 | ```swift 4 | public func /(lhs: CGSize, rhs: CGFloat) -> CGSize 5 | ``` 6 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/methods/dprint(__).md: -------------------------------------------------------------------------------- 1 | ### `dprint(_:)` 2 | 3 | ```swift 4 | public func dprint(_ item: @autoclosure () -> Any) 5 | ``` 6 | 7 | Swift still calls `print()` and/or `debugPrint()` in shipped apps. 8 | We use a method described in onevcat's post (https://onevcat.com/2016/02/swift-performance/) 9 | to optimaze the performance. 10 | 11 | - Parameter item: items to print 12 | 13 | #### Parameters 14 | 15 | | Name | Description | 16 | | ---- | ----------- | 17 | | item | items to print | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/methods/dprint(____).md: -------------------------------------------------------------------------------- 1 | ### `dprint(_:_:)` 2 | 3 | ```swift 4 | public func dprint(_ item: @autoclosure () -> Any, _ event: PrintEvent = .d) 5 | ``` 6 | 7 | Swift still calls `print()` and/or `debugPrint()` in shipped apps. 8 | We use a method described in onevcat's post (https://onevcat.com/2016/02/swift-performance/) 9 | to optimaze the performance. 10 | 11 | - Parameter item: items to print 12 | 13 | #### Parameters 14 | 15 | | Name | Description | 16 | | ---- | ----------- | 17 | | item | items to print | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/methods/sleep(duration_).md: -------------------------------------------------------------------------------- 1 | ### `sleep(duration:)` 2 | 3 | ```swift 4 | public func sleep(duration: TimeInterval) 5 | ``` 6 | 7 | Sleeps the thread 8 | - Parameter duration: in seconds 9 | 10 | #### Parameters 11 | 12 | | Name | Description | 13 | | ---- | ----------- | 14 | | duration | in seconds | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/protocols/TimerStartable.md: -------------------------------------------------------------------------------- 1 | **PROTOCOL** 2 | 3 | # `TimerStartable` 4 | 5 | ```swift 6 | public protocol TimerStartable 7 | ``` 8 | 9 | ## Methods 10 | ### `start(totalTime:)` 11 | 12 | ```swift 13 | func start(totalTime: TimeInterval?) -> AnyPublisher 14 | ``` 15 | 16 | ### `stop()` 17 | 18 | ```swift 19 | func stop() 20 | ``` 21 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/structs/KeyboardInfo.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `KeyboardInfo` 4 | 5 | ```swift 6 | public struct KeyboardInfo: Equatable 7 | ``` 8 | 9 | Struct modeling keyboard updates 10 | 11 | ## Properties 12 | ### `height` 13 | 14 | ```swift 15 | public var height: CGFloat = 0 16 | ``` 17 | 18 | Keyboard height 19 | 20 | ### `animationCurve` 21 | 22 | ```swift 23 | public var animationCurve: UIView.AnimationCurve = UIView.AnimationCurve.easeInOut 24 | ``` 25 | 26 | Keyboard animation curve 27 | 28 | ### `animationDuration` 29 | 30 | ```swift 31 | public var animationDuration: TimeInterval = 0.0 32 | ``` 33 | 34 | Keyboard animation duration 35 | 36 | ### `isVisible` 37 | 38 | ```swift 39 | public var isVisible: Bool 40 | ``` 41 | 42 | Is the keyboard visible 43 | 44 | ## Methods 45 | ### `init()` 46 | 47 | ```swift 48 | public init() 49 | ``` 50 | 51 | Create new 52 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/structs/Notification.KeyboardInfo.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `Notification.KeyboardInfo` 4 | 5 | ```swift 6 | public struct KeyboardInfo 7 | ``` 8 | 9 | Struct modeling keyboard updates 10 | 11 | ## Properties 12 | ### `height` 13 | 14 | ```swift 15 | public var height: CGFloat = 0 16 | ``` 17 | 18 | Keyboard height 19 | 20 | ### `animationCurve` 21 | 22 | ```swift 23 | public var animationCurve: UIView.AnimationCurve = UIView.AnimationCurve.easeInOut 24 | ``` 25 | 26 | Keyboard animation curve 27 | 28 | ### `animationDuration` 29 | 30 | ```swift 31 | public var animationDuration: TimeInterval = 0.0 32 | ``` 33 | 34 | Keyboard animation duration 35 | 36 | ### `isVisible` 37 | 38 | ```swift 39 | public var isVisible: Bool 40 | ``` 41 | 42 | Is the keyboard visible 43 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/structs/Preview.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `Preview` 4 | 5 | ```swift 6 | public struct Preview: View 7 | ``` 8 | 9 | Preview content on iPhone 12 pro in light mode, iPhone 12 mini in dark mode 10 | 11 | ## Properties 12 | ### `body` 13 | 14 | ```swift 15 | public var body: some View 16 | ``` 17 | 18 | ## Methods 19 | ### `init(_:)` 20 | 21 | ```swift 22 | public init(@ViewBuilder _ content: () -> Content) 23 | ``` 24 | 25 | Create a preview for iPhone 12 pro and iPhone 12 mini 26 | - Parameter content: Content 27 | 28 | #### Parameters 29 | 30 | | Name | Description | 31 | | ---- | ----------- | 32 | | content | Content | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/structs/Screen.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `Screen` 4 | 5 | ```swift 6 | public struct Screen 7 | ``` 8 | 9 | Wrapper to access main screen size 10 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/structs/UserDefault.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `UserDefault` 4 | 5 | ```swift 6 | public struct UserDefault 7 | ``` 8 | 9 | ``` 10 | @UserDefault("key") var myInt = 0 11 | ``` 12 | 13 | ## Properties 14 | ### `key` 15 | 16 | ```swift 17 | public let key: String 18 | ``` 19 | 20 | Key 21 | 22 | ### `wrappedValue` 23 | 24 | ```swift 25 | public var wrappedValue: T? 26 | ``` 27 | 28 | Get the value if it existes 29 | 30 | ## Methods 31 | ### `init(_:)` 32 | 33 | ```swift 34 | public init(_ key: String) 35 | ``` 36 | 37 | Create a new Default with Key 38 | - Parameter key: String key 39 | 40 | #### Parameters 41 | 42 | | Name | Description | 43 | | ---- | ----------- | 44 | | key | String key | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/typealiases/Subscribers.ReceivedCompletion.md: -------------------------------------------------------------------------------- 1 | **TYPEALIAS** 2 | 3 | # `Subscribers.ReceivedCompletion` 4 | 5 | ```swift 6 | typealias ReceivedCompletion = (Subscribers.Completion) -> () 7 | ``` 8 | -------------------------------------------------------------------------------- /Documentation/Reference/ExtensionKit/typealiases/Subscribers.ReceivedValue.md: -------------------------------------------------------------------------------- 1 | **TYPEALIAS** 2 | 3 | # `Subscribers.ReceivedValue` 4 | 5 | ```swift 6 | typealias ReceivedValue = (Input) -> () 7 | ``` 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | gem "fastlane" 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.3) 5 | addressable (2.7.0) 6 | public_suffix (>= 2.0.2, < 5.0) 7 | artifactory (3.0.15) 8 | atomos (0.1.3) 9 | aws-eventstream (1.1.1) 10 | aws-partitions (1.451.0) 11 | aws-sdk-core (3.114.0) 12 | aws-eventstream (~> 1, >= 1.0.2) 13 | aws-partitions (~> 1, >= 1.239.0) 14 | aws-sigv4 (~> 1.1) 15 | jmespath (~> 1.0) 16 | aws-sdk-kms (1.43.0) 17 | aws-sdk-core (~> 3, >= 3.112.0) 18 | aws-sigv4 (~> 1.1) 19 | aws-sdk-s3 (1.94.1) 20 | aws-sdk-core (~> 3, >= 3.112.0) 21 | aws-sdk-kms (~> 1) 22 | aws-sigv4 (~> 1.1) 23 | aws-sigv4 (1.2.3) 24 | aws-eventstream (~> 1, >= 1.0.2) 25 | babosa (1.0.4) 26 | claide (1.0.3) 27 | colored (1.2) 28 | colored2 (3.1.2) 29 | commander-fastlane (4.4.6) 30 | highline (~> 1.7.2) 31 | declarative (0.0.20) 32 | digest-crc (0.6.3) 33 | rake (>= 12.0.0, < 14.0.0) 34 | domain_name (0.5.20190701) 35 | unf (>= 0.0.5, < 1.0.0) 36 | dotenv (2.7.6) 37 | emoji_regex (3.2.2) 38 | excon (0.81.0) 39 | faraday (1.4.1) 40 | faraday-excon (~> 1.1) 41 | faraday-net_http (~> 1.0) 42 | faraday-net_http_persistent (~> 1.1) 43 | multipart-post (>= 1.2, < 3) 44 | ruby2_keywords (>= 0.0.4) 45 | faraday-cookie_jar (0.0.7) 46 | faraday (>= 0.8.0) 47 | http-cookie (~> 1.0.0) 48 | faraday-excon (1.1.0) 49 | faraday-net_http (1.0.1) 50 | faraday-net_http_persistent (1.1.0) 51 | faraday_middleware (1.0.0) 52 | faraday (~> 1.0) 53 | fastimage (2.2.3) 54 | fastlane (2.181.0) 55 | CFPropertyList (>= 2.3, < 4.0.0) 56 | addressable (>= 2.3, < 3.0.0) 57 | artifactory (~> 3.0) 58 | aws-sdk-s3 (~> 1.0) 59 | babosa (>= 1.0.3, < 2.0.0) 60 | bundler (>= 1.12.0, < 3.0.0) 61 | colored 62 | commander-fastlane (>= 4.4.6, < 5.0.0) 63 | dotenv (>= 2.1.1, < 3.0.0) 64 | emoji_regex (>= 0.1, < 4.0) 65 | excon (>= 0.71.0, < 1.0.0) 66 | faraday (~> 1.0) 67 | faraday-cookie_jar (~> 0.0.6) 68 | faraday_middleware (~> 1.0) 69 | fastimage (>= 2.1.0, < 3.0.0) 70 | gh_inspector (>= 1.1.2, < 2.0.0) 71 | google-api-client (>= 0.37.0, < 0.39.0) 72 | google-cloud-storage (>= 1.15.0, < 2.0.0) 73 | highline (>= 1.7.2, < 2.0.0) 74 | json (< 3.0.0) 75 | jwt (>= 2.1.0, < 3) 76 | mini_magick (>= 4.9.4, < 5.0.0) 77 | multipart-post (~> 2.0.0) 78 | naturally (~> 2.2) 79 | plist (>= 3.1.0, < 4.0.0) 80 | rubyzip (>= 2.0.0, < 3.0.0) 81 | security (= 0.1.3) 82 | simctl (~> 1.6.3) 83 | slack-notifier (>= 2.0.0, < 3.0.0) 84 | terminal-notifier (>= 2.0.0, < 3.0.0) 85 | terminal-table (>= 1.4.5, < 2.0.0) 86 | tty-screen (>= 0.6.3, < 1.0.0) 87 | tty-spinner (>= 0.8.0, < 1.0.0) 88 | word_wrap (~> 1.0.0) 89 | xcodeproj (>= 1.13.0, < 2.0.0) 90 | xcpretty (~> 0.3.0) 91 | xcpretty-travis-formatter (>= 0.0.3) 92 | gh_inspector (1.1.3) 93 | google-api-client (0.38.0) 94 | addressable (~> 2.5, >= 2.5.1) 95 | googleauth (~> 0.9) 96 | httpclient (>= 2.8.1, < 3.0) 97 | mini_mime (~> 1.0) 98 | representable (~> 3.0) 99 | retriable (>= 2.0, < 4.0) 100 | signet (~> 0.12) 101 | google-apis-core (0.3.0) 102 | addressable (~> 2.5, >= 2.5.1) 103 | googleauth (~> 0.14) 104 | httpclient (>= 2.8.1, < 3.0) 105 | mini_mime (~> 1.0) 106 | representable (~> 3.0) 107 | retriable (>= 2.0, < 4.0) 108 | rexml 109 | signet (~> 0.14) 110 | webrick 111 | google-apis-iamcredentials_v1 (0.3.0) 112 | google-apis-core (~> 0.1) 113 | google-apis-storage_v1 (0.3.0) 114 | google-apis-core (~> 0.1) 115 | google-cloud-core (1.6.0) 116 | google-cloud-env (~> 1.0) 117 | google-cloud-errors (~> 1.0) 118 | google-cloud-env (1.5.0) 119 | faraday (>= 0.17.3, < 2.0) 120 | google-cloud-errors (1.1.0) 121 | google-cloud-storage (1.31.0) 122 | addressable (~> 2.5) 123 | digest-crc (~> 0.4) 124 | google-apis-iamcredentials_v1 (~> 0.1) 125 | google-apis-storage_v1 (~> 0.1) 126 | google-cloud-core (~> 1.2) 127 | googleauth (~> 0.9) 128 | mini_mime (~> 1.0) 129 | googleauth (0.16.2) 130 | faraday (>= 0.17.3, < 2.0) 131 | jwt (>= 1.4, < 3.0) 132 | memoist (~> 0.16) 133 | multi_json (~> 1.11) 134 | os (>= 0.9, < 2.0) 135 | signet (~> 0.14) 136 | highline (1.7.10) 137 | http-cookie (1.0.3) 138 | domain_name (~> 0.5) 139 | httpclient (2.8.3) 140 | jmespath (1.4.0) 141 | json (2.5.1) 142 | jwt (2.2.3) 143 | memoist (0.16.2) 144 | mini_magick (4.11.0) 145 | mini_mime (1.1.0) 146 | multi_json (1.15.0) 147 | multipart-post (2.0.0) 148 | nanaimo (0.3.0) 149 | naturally (2.2.1) 150 | os (1.1.1) 151 | plist (3.6.0) 152 | public_suffix (4.0.6) 153 | rake (13.0.3) 154 | representable (3.1.1) 155 | declarative (< 0.1.0) 156 | trailblazer-option (>= 0.1.1, < 0.2.0) 157 | uber (< 0.2.0) 158 | retriable (3.1.2) 159 | rexml (3.2.5) 160 | rouge (2.0.7) 161 | ruby2_keywords (0.0.4) 162 | rubyzip (2.3.0) 163 | security (0.1.3) 164 | signet (0.15.0) 165 | addressable (~> 2.3) 166 | faraday (>= 0.17.3, < 2.0) 167 | jwt (>= 1.5, < 3.0) 168 | multi_json (~> 1.10) 169 | simctl (1.6.8) 170 | CFPropertyList 171 | naturally 172 | slack-notifier (2.3.2) 173 | terminal-notifier (2.0.0) 174 | terminal-table (1.8.0) 175 | unicode-display_width (~> 1.1, >= 1.1.1) 176 | trailblazer-option (0.1.1) 177 | tty-cursor (0.7.1) 178 | tty-screen (0.8.1) 179 | tty-spinner (0.9.3) 180 | tty-cursor (~> 0.7) 181 | uber (0.1.0) 182 | unf (0.1.4) 183 | unf_ext 184 | unf_ext (0.0.7.7) 185 | unicode-display_width (1.7.0) 186 | webrick (1.7.0) 187 | word_wrap (1.0.0) 188 | xcodeproj (1.19.0) 189 | CFPropertyList (>= 2.3.3, < 4.0) 190 | atomos (~> 0.1.3) 191 | claide (>= 1.0.2, < 2.0) 192 | colored2 (~> 3.1) 193 | nanaimo (~> 0.3.0) 194 | xcpretty (0.3.0) 195 | rouge (~> 2.0.7) 196 | xcpretty-travis-formatter (1.0.1) 197 | xcpretty (~> 0.2, >= 0.0.7) 198 | 199 | PLATFORMS 200 | arm64-darwin-21 201 | universal-darwin-20 202 | 203 | DEPENDENCIES 204 | fastlane 205 | 206 | BUNDLED WITH 207 | 2.3.9 208 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ExtensionKit", 8 | platforms: [.iOS(.v16)], 9 | products: [ 10 | .library( 11 | name: "ExtensionKit", 12 | targets: ["ExtensionKit"]), 13 | ], 14 | targets: [ 15 | .target( 16 | name: "ExtensionKit", 17 | dependencies: []), 18 | .testTarget( 19 | name: "ExtensionKitTests", 20 | dependencies: ["ExtensionKit"]), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Release](https://img.shields.io/github/release/gtokman/extensionkit.svg)](https://github.com/gtokman/extensionkit/releases) 3 | ![Swift 5.7](https://img.shields.io/badge/Swift-5.7-orange.svg) 4 | [![Swift Package Manager Compatible](https://img.shields.io/badge/spm-compatible-brightgreen.svg)](https://swift.org/package-manager) 5 | ![Supported Platform](https://img.shields.io/badge/platform-iOS-lightgrey) 6 | 7 | Screen Shot 2021-04-24 at 3 58 58 PM 8 | 9 | # Documentation 10 | 11 | ExtensionKit includes many extensions, from getting the user **[location](Documentation/Reference/ExtensionKit/extensions/CLLocationManager.md#receivelocationupdatesonetime) with a deterministic Combine API** to a **[shimmer](Documentation/Reference/ExtensionKit/extensions/View.md#shimmerisactivespeedangle) loading animation**, to **[keyboard](Documentation/Reference/ExtensionKit/extensions/View.md#bottomsheetispresentedheightanimationthumbhiddencontent)** and much much more. Check out the docs below or install the library with SPM to try it out. 12 | 13 | Many extensions are things I find cumbersome in everyday iOS dev so I made a package where I can reference the them anytime. Lastly, a lot of these are inspired or taken from open source projects and articles from the Swift community ❤️ 14 | 15 |
16 | 17 | ## Extensions 18 | 19 | ### SwiftUI 20 | 21 | - [Button](Documentation/Reference/ExtensionKit/extensions/Button.md) 22 | - [Binding](Documentation/Reference/ExtensionKit/extensions/Binding.md) 23 | - [Color](Documentation/Reference/ExtensionKit/extensions/Color.md) 24 | - [GeometryProxy](Documentation/Reference/ExtensionKit/extensions/GeometryProxy.md) 25 | - [Preview](Documentation/Reference/ExtensionKit/structs/Preview.md) 26 | - [Image](Documentation/Reference/ExtensionKit/extensions/Image.md) 27 | - [Shape](Documentation/Reference/ExtensionKit/extensions/Shape.md) 28 | - [Text](Documentation/Reference/ExtensionKit/extensions/Text.md) 29 | - [View](Documentation/Reference/ExtensionKit/extensions/View.md) 30 | 31 | ### Combine 32 | 33 | - [Publisher](Documentation/Reference/ExtensionKit/extensions/Publisher.md) 34 | 35 | ### Property Wrappers 36 | 37 | - [UserDefault](Documentation/Reference/ExtensionKit/structs/UserDefault.md) 38 | 39 | ### UIKit 40 | 41 | - [UIApplication](Documentation/Reference/ExtensionKit/extensions/UIApplication.md) 42 | - [UIButton](Documentation/Reference/ExtensionKit/extensions/UIButton.md) 43 | - [UIColor](Documentation/Reference/ExtensionKit/extensions/UIColor.md) 44 | - [UIImage](Documentation/Reference/ExtensionKit/extensions/UIImage.md) 45 | - [UITableView](Documentation/Reference/ExtensionKit/extensions/UITableView.md) 46 | - [UIViewController](Documentation/Reference/ExtensionKit/extensions/UIViewController.md) 47 | - [UIView](Documentation/Reference/ExtensionKit/extensions/UIView.md) 48 | 49 | ### Foundation 50 | 51 | - [Array](Documentation/Reference/ExtensionKit/extensions/Array.md) 52 | - [Bundle](Documentation/Reference/ExtensionKit/extensions/Bundle.md) 53 | - [CFRunLoopTimer](Documentation/Reference/ExtensionKit/extensions/CFRunLoopTimer.md) 54 | - [Collection](Documentation/Reference/ExtensionKit/extensions/Collection.md) 55 | - [Data](Documentation/Reference/ExtensionKit/extensions/Data.md) 56 | - [Date](Documentation/Reference/ExtensionKit/extensions/Date.md) 57 | - [DispactQueue](Documentation/Reference/ExtensionKit/extensions/DispactQueue.md) 58 | - [FileManager](Documentation/Reference/ExtensionKit/extensions/FileManager.md) 59 | - [Int](Documentation/Reference/ExtensionKit/extensions/Int.md) 60 | - [NSObject](Documentation/Reference/ExtensionKit/extensions/NSObject.md) 61 | - [UserDefaults](Documentation/Reference/ExtensionKit/extensions/UserDefaults.md) 62 | - [RandomAccessCollection](Documentation/Reference/ExtensionKit/extensions/RandomAccessCollection.md) 63 | - [String](Documentation/Reference/ExtensionKit/extensions/String.md) 64 | - [String.Index](Documentation/Reference/ExtensionKit/extensions/String.Index.md) 65 | - [Timer](Documentation/Reference/ExtensionKit/extensions/Timer.md) 66 | - [URL](Documentation/Reference/ExtensionKit/extensions/URL.md) 67 | 68 | ### CoreGraphics 69 | 70 | - [CGPoint](Documentation/Reference/ExtensionKit/extensions/CGPoint.md) 71 | - [CGRect](Documentation/Reference/ExtensionKit/extensions/CGRect.md) 72 | 73 | ### CoreLocation 74 | 75 | - [CLLocation](Documentation/Reference/ExtensionKit/extensions/CLLocation.md) 76 | - [CLLocationManager](Documentation/Reference/ExtensionKit/extensions/CLLocationManager.md) 77 | - [CLLocationCoordinate2D](Documentation/Reference/ExtensionKit/extensions/CLLocationCoordinate2D.md) 78 | 79 | ## Methods 80 | 81 | - [+](Documentation/Reference/ExtensionKit/methods/+(____).md) 82 | - [-](Documentation/Reference/ExtensionKit/methods/-(____).md) 83 | - [/](Documentation/Reference/ExtensionKit/methods/_(____).md) 84 | - [*](Documentation/Reference/ExtensionKit/methods/_(____).md) 85 | - [dprint(__)](Documentation/Reference/ExtensionKit/methods/dprint(__).md) 86 | - [sleep(duration_)](Documentation/Reference/ExtensionKit/methods/sleep(duration_).md) 87 | 88 | ## Structs 89 | 90 | - [Screen](Documentation/Reference/ExtensionKit/structs/Screen.md) 91 | - [KeyboardInfo](Documentation/Reference/ExtensionKit/structs/KeyboardInfo.md) 92 | 93 | 94 | ## Installation 95 | 96 | Add the url for ExtensionKit in Xcode with SPM and `Import ExtensionKit` to start using it (iOS 13+). 97 |
98 | 99 | [![Release](https://img.shields.io/github/release/gtokman/extensionkit.svg)](https://github.com/gtokman/extensionkit/releases) 100 | 101 | 102 | ``` 103 | https://github.com/gtokman/ExtensionKit.git 104 | ``` 105 | 106 | 107 | ## Contact 108 | Follow and contact me on Twitter at [@f6ary](https://www.twitter.com/f6ary). 109 | 110 | 111 | ## Contributions 112 | If you find an issue, just [open a ticket](https://github.com/gtokman/extensionkit/issues/new) 113 | on it. Pull requests are warmly welcome as well, especially SwiftUI or AVFoundation! 114 | 115 | **Steps** 116 | 117 | 1. Clone repo 118 | 2. **Important** pls use [semantic commit](https://github.com/conventional-changelog/commitlint#what-is-commitlint) messages (ex: `feat: your description`) 119 | 3. Add extension to [source](Sources/ExtensionKit/) directory 120 | 4. Tests are optional but add any in the [Tests](Tests) directory 121 | 5. Add documentation to the extension (opt + cmd + /) in Xcode 122 | 6. Submit a PR 123 | 7. After it merges, I'll cut a new release and update the docs 124 | 125 | ## License 126 | ExtensionKit is licensed under the MIT license. 127 | -------------------------------------------------------------------------------- /Sources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtokman/ExtensionKit/6198a989e661416a64f7523710bcf07c67e7e2e6/Sources/.gitkeep -------------------------------------------------------------------------------- /Sources/ExtensionKit/AutoLayout/LayoutAnchor.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public protocol LayoutAnchor { 4 | func constraint(equalTo anchor: Self, constant: CGFloat) -> NSLayoutConstraint 5 | func constraint(greaterThanOrEqualTo anchor: Self, constant: CGFloat) -> NSLayoutConstraint 6 | func constraint(lessThanOrEqualTo anchor: Self, constant: CGFloat) -> NSLayoutConstraint 7 | } 8 | 9 | extension NSLayoutAnchor: LayoutAnchor {} 10 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/AutoLayout/LayoutClosure.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UIView { 4 | func layout(using closure: (LayoutProxy) -> Void) { 5 | translatesAutoresizingMaskIntoConstraints = false 6 | closure(LayoutProxy(view: self)) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/AutoLayout/LayoutDimension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public protocol LayoutDimension { 4 | func constraint(equalToConstant c: CGFloat) -> NSLayoutConstraint 5 | func constraint(lessThanOrEqualToConstant: CGFloat) -> NSLayoutConstraint 6 | func constraint(greaterThanOrEqualToConstant: CGFloat) -> NSLayoutConstraint 7 | } 8 | 9 | extension NSLayoutDimension: LayoutDimension {} 10 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/AutoLayout/LayoutOperators.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// This files enables comparison operators amongst anchors and adding/subtracting of constants 4 | 5 | public func +(lhs: A, rhs: CGFloat) -> (A, CGFloat) { 6 | return (lhs, rhs) 7 | } 8 | 9 | public func -(lhs: A, rhs: CGFloat) -> (A, CGFloat) { 10 | return (lhs, -rhs) 11 | } 12 | 13 | public func ==(lhs: LayoutProperty, rhs: (A, CGFloat)) { 14 | lhs.equal(to: rhs.0, offsetBy: rhs.1) 15 | } 16 | 17 | public func ==(lhs: LayoutProperty, rhs: A) { 18 | lhs.equal(to: rhs) 19 | } 20 | 21 | public func ==(lhs: LayoutProperty, rhs: CGFloat) { 22 | lhs.equalToConstant(rhs) 23 | } 24 | 25 | public func >=(lhs: LayoutProperty, rhs: (A, CGFloat)) { 26 | lhs.greaterThanOrEqual(to: rhs.0, offsetBy: rhs.1) 27 | } 28 | 29 | public func >=(lhs: LayoutProperty, rhs: A) { 30 | lhs.greaterThanOrEqual(to: rhs) 31 | } 32 | 33 | public func >=(lhs: LayoutProperty, rhs: CGFloat) { 34 | lhs.greaterThanOrEqualToConstant(rhs) 35 | } 36 | 37 | public func <=(lhs: LayoutProperty, rhs: (A, CGFloat)) { 38 | lhs.lessThanOrEqual(to: rhs.0, offsetBy: rhs.1) 39 | } 40 | 41 | public func <=(lhs: LayoutProperty, rhs: A) { 42 | lhs.lessThanOrEqual(to: rhs) 43 | } 44 | 45 | public func <=(lhs: LayoutProperty, rhs: CGFloat) { 46 | lhs.lessThanOrEqualToConstant(rhs) 47 | } 48 | 49 | public func ==(lhs: LayoutProperty, rhs: CGSize) { 50 | lhs.equalToConstant(rhs) 51 | } 52 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/AutoLayout/LayoutProperty.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public struct LayoutProperty { 4 | let anchor: Anchor 5 | } 6 | 7 | extension LayoutProperty where Anchor: LayoutAnchor { 8 | func equal(to otherAnchor: Anchor, offsetBy 9 | constant: CGFloat = 0) 10 | { 11 | anchor.constraint(equalTo: otherAnchor, 12 | constant: constant).isActive = true 13 | } 14 | 15 | func greaterThanOrEqual(to otherAnchor: Anchor, 16 | offsetBy constant: CGFloat = 0) 17 | { 18 | anchor.constraint(greaterThanOrEqualTo: otherAnchor, 19 | constant: constant).isActive = true 20 | } 21 | 22 | func lessThanOrEqual(to otherAnchor: Anchor, 23 | offsetBy constant: CGFloat = 0) 24 | { 25 | anchor.constraint(lessThanOrEqualTo: otherAnchor, 26 | constant: constant).isActive = true 27 | } 28 | } 29 | 30 | extension LayoutProperty where Anchor: LayoutDimension { 31 | func equalToConstant(_ c: CGFloat) { 32 | anchor.constraint(equalToConstant: c).isActive = true 33 | } 34 | 35 | func lessThanOrEqualToConstant(_ c: CGFloat) { 36 | anchor.constraint(lessThanOrEqualToConstant: c).isActive = true 37 | } 38 | 39 | func greaterThanOrEqualToConstant(_ c: CGFloat) { 40 | anchor.constraint(greaterThanOrEqualToConstant: c).isActive = true 41 | } 42 | } 43 | 44 | extension LayoutProperty where Anchor: LayoutSizeDimension { 45 | func equalToConstant(_ c: CGSize) { 46 | let constraints = anchor.constraint(equalToConstant: c) 47 | constraints.height.isActive = true 48 | constraints.width.isActive = true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/AutoLayout/LayoutProxy.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public class LayoutProxy { 4 | public lazy var leading = LayoutProperty(anchor: view.leadingAnchor) 5 | public lazy var trailing = LayoutProperty(anchor: view.trailingAnchor) 6 | public lazy var top = LayoutProperty(anchor: view.topAnchor) 7 | public lazy var bottom = LayoutProperty(anchor: view.bottomAnchor) 8 | public lazy var width = LayoutProperty(anchor: view.widthAnchor) 9 | public lazy var height = LayoutProperty(anchor: view.heightAnchor) 10 | public lazy var centerX = LayoutProperty(anchor: view.centerXAnchor) 11 | public lazy var centerY = LayoutProperty(anchor: view.centerYAnchor) 12 | public lazy var size = LayoutProperty( 13 | anchor: LayoutSizeDimensionAnchor( 14 | heightAnchor: view.heightAnchor, 15 | widthAnchor: view.widthAnchor 16 | ) 17 | ) 18 | 19 | private let view: UIView 20 | 21 | init(view: UIView) { 22 | self.view = view 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/AutoLayout/LayoutSizeDimension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public protocol LayoutSizeDimension { 4 | func constraint(equalToConstant c: CGSize) -> (height: NSLayoutConstraint, width: NSLayoutConstraint) 5 | } 6 | 7 | public struct LayoutSizeDimensionAnchor: LayoutSizeDimension { 8 | let heightAnchor: NSLayoutDimension 9 | let widthAnchor: NSLayoutDimension 10 | 11 | public func constraint( 12 | equalToConstant c: CGSize 13 | ) -> ( 14 | height: NSLayoutConstraint, 15 | width: NSLayoutConstraint 16 | ) { 17 | return ( 18 | height: heightAnchor.constraint(equalToConstant: c.height), 19 | width: widthAnchor.constraint(equalToConstant: c.width) 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/AutoLayout/UIView+Fill.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | @discardableResult 5 | public func fillWith( 6 | _ view: UIView, 7 | insets: UIEdgeInsets? = nil 8 | ) -> ( 9 | top: NSLayoutConstraint, 10 | bottom: NSLayoutConstraint, 11 | leading: NSLayoutConstraint, 12 | trailing: NSLayoutConstraint 13 | ) { 14 | view.translatesAutoresizingMaskIntoConstraints = false 15 | addSubview(view) 16 | 17 | let topInset = insets?.top ?? 0 18 | let bottomInset = -(insets?.bottom ?? 0) 19 | let leadingInset = insets?.left ?? 0 20 | let trailingInset = -(insets?.right ?? 0) 21 | 22 | let top = view.topAnchor.constraint(equalTo: topAnchor, constant: topInset) 23 | let bottom = view.bottomAnchor.constraint(equalTo: bottomAnchor, constant: bottomInset) 24 | let leading = view.leadingAnchor.constraint(equalTo: leadingAnchor, constant: leadingInset) 25 | let trailing = view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: trailingInset) 26 | 27 | NSLayoutConstraint.activate([top, bottom, leading, trailing]) 28 | 29 | return (top: top, bottom: bottom, leading: leading, trailing: trailing) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Combine/Publisher.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | #if canImport(UIKit) 4 | import UIKit 5 | #elseif canImport(AppKit) 6 | import AppKit 7 | #endif 8 | 9 | // MARK: - Public 10 | 11 | public extension Publisher { 12 | 13 | /// Debug print Publisher events 14 | /// - Parameters: 15 | /// - prefix: Prefix on print statement 16 | /// - function: Function name 17 | /// - line: Line number 18 | /// - Returns: Publisher 19 | func debug( 20 | prefix: String = "", 21 | function: String = #function, 22 | line: Int = #line) -> Publishers.HandleEvents { 23 | let pattern = "\(prefix + (prefix.isEmpty ? "" : " "))\(function), line \(line): " 24 | 25 | return handleEvents(receiveSubscription: { 26 | dprint("\(pattern)subscription \($0)") 27 | }, receiveOutput: { 28 | dprint("\(pattern)output \($0)") 29 | }, receiveCompletion: { 30 | dprint("\(pattern)completion \($0)") 31 | }, receiveCancel: { 32 | dprint("\(pattern)cancelled") 33 | }, receiveRequest: { 34 | dprint("\(pattern)request \($0)") 35 | }) 36 | } 37 | 38 | } 39 | 40 | public extension Publisher where Failure == Never { 41 | 42 | /// Receive Output value on main thread (DispatchQueue.main) 43 | func receiveOnMain() -> Publishers.ReceiveOn { 44 | self.receive(on: DispatchQueue.main) 45 | } 46 | 47 | /// A single value sink function outputs a `Result` 48 | /// - Parameter result: Result 49 | /// - Returns: AnyCancellable 50 | func sink(result: @escaping ((Result) -> Void)) -> AnyCancellable { 51 | return sink(receiveCompletion: { completion in 52 | switch completion { 53 | case .failure(let error): 54 | result(.failure(error)) 55 | case .finished: break 56 | } 57 | }, receiveValue: { output in 58 | result(.success(output)) 59 | }) 60 | } 61 | 62 | /// Assign property to object without using [weak self] 63 | /// - Parameters: 64 | /// - keyPath: Property keypath 65 | /// - root: Any object 66 | /// - Returns: AnyCancellable 67 | func assign(to keyPath: ReferenceWritableKeyPath, on root: Root) -> AnyCancellable { 68 | sink { [weak root] in 69 | root?[keyPath: keyPath] = $0 70 | } 71 | } 72 | 73 | } 74 | 75 | public protocol TimerStartable { 76 | func start(totalTime: TimeInterval?) -> AnyPublisher 77 | func stop() 78 | } 79 | 80 | /// AutoConnect publishers that have TimerPublisher as its their upstream aka "autoconnect()" 81 | extension Publishers.Autoconnect: TimerStartable where Upstream: Timer.TimerPublisher { 82 | 83 | /// Stops timer of current timer publisher instance 84 | public func stop() { 85 | upstream.connect().cancel() 86 | } 87 | 88 | /// Stops timer of current timer publisher instance after totalTime 89 | /// If totalTime is nil, then continues until hard stopped 90 | public func start(totalTime: TimeInterval? = nil) -> AnyPublisher { 91 | var timeElapsed: TimeInterval = 0 92 | return flatMap { date in 93 | return Future { promise in 94 | if let totalTime = totalTime, timeElapsed >= totalTime { self.stop() } 95 | promise(.success(date)) 96 | timeElapsed += 1 97 | } 98 | }.eraseToAnyPublisher() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Combine/Subscriber.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | /// Limited Sink subscriber that only takes in a specified amount of demand 4 | public extension Subscribers { 5 | typealias ReceivedCompletion = (Subscribers.Completion) -> () 6 | typealias ReceivedValue = (Input) -> () 7 | 8 | class LimitedSink: Subscriber, Cancellable where Failure: Error { 9 | 10 | private let demand: Int 11 | private let receivedValue: ReceivedValue 12 | private var receivedCompletion: ReceivedCompletion? 13 | private var subscription: Subscription? 14 | 15 | init(demand: Int, receivedValue: @escaping ReceivedValue){ 16 | self.demand = demand 17 | self.receivedValue = receivedValue 18 | } 19 | 20 | convenience init(demand: Int, receivedCompletion: @escaping ReceivedCompletion, receivedValue: @escaping ReceivedValue) { 21 | self.init(demand: demand, receivedValue: receivedValue) 22 | self.receivedCompletion = receivedCompletion 23 | } 24 | 25 | public func receive(subscription: Subscription) { 26 | // Request demand from subscription 27 | self.subscription = subscription 28 | subscription.request(.max(demand)) 29 | } 30 | 31 | public func receive(_ input: Input) -> Subscribers.Demand { 32 | receivedValue(input) 33 | return .none 34 | } 35 | 36 | public func receive(completion: Subscribers.Completion) { 37 | receivedCompletion?(completion) 38 | } 39 | 40 | public func cancel() { 41 | subscription?.cancel() 42 | subscription = nil 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/CoreGraphics/CGPoint.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | public extension CGPoint { 4 | 5 | /// Offset point by new x and y 6 | /// - Parameters: 7 | /// - x: x 8 | /// - y: y 9 | /// - Returns: new point 10 | func offseted(x: CGFloat = 0.0, y: CGFloat = 0.0) -> CGPoint { 11 | var point = self 12 | point.x += x 13 | point.y += y 14 | return point 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/CoreGraphics/CGRect.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | public extension CGRect { 4 | 5 | /// Main `aspectFit` method that decides whether to fit by width or height. 6 | /// Used by the mask view in `SharedTransitionAnimationController`. 7 | /// 8 | func aspectFit(to frame: CGRect) -> CGRect { 9 | let ratio = width / height 10 | let frameRatio = frame.width / frame.height 11 | 12 | // If target frame is narrower than original, fit to width, 13 | // else if target frame is wider than original, fit to height 14 | if frameRatio < ratio { 15 | return aspectFitWidth(to: frame) 16 | } else { 17 | return aspectFitHeight(to: frame) 18 | } 19 | } 20 | 21 | // Fits the rect to the target frame's width while maintaining aspect ratio, 22 | // and centers the result vertically in the target frame 23 | func aspectFitWidth(to frame: CGRect) -> CGRect { 24 | let ratio = width / height 25 | let height = frame.width * ratio 26 | let offsetY = (frame.height - height) / 2 // Center vertically 27 | let origin = CGPoint(x: frame.origin.x, y: frame.origin.y + offsetY) 28 | let size = CGSize(width: frame.width, height: height) 29 | return CGRect(origin: origin, size: size) 30 | } 31 | 32 | // Fits the rect to the target frame's height while maintaining aspect ratio, 33 | // and cnters the result horizontally in the target frame 34 | func aspectFitHeight(to frame: CGRect) -> CGRect { 35 | let ratio = height / width 36 | let width = frame.height * ratio 37 | let offsetX = (frame.width - width) / 2 // Center horizontally 38 | let origin = CGPoint(x: frame.origin.x + offsetX, y: frame.origin.y) 39 | let size = CGSize(width: width, height: frame.height) 40 | return CGRect(origin: origin, size: size) 41 | } 42 | 43 | var topLeft: CGPoint { 44 | return origin 45 | } 46 | 47 | var topRight: CGPoint { 48 | return CGPoint(x: maxX, y: minY) 49 | } 50 | 51 | var topMiddle: CGPoint { 52 | return CGPoint(x: midX, y: minY) 53 | } 54 | 55 | var bottomLeft: CGPoint { 56 | return CGPoint(x: minX, y: maxY) 57 | } 58 | 59 | var bottomRight: CGPoint { 60 | return CGPoint(x: maxX, y: maxY) 61 | } 62 | 63 | var bottomMiddle: CGPoint { 64 | return CGPoint(x: midX, y: maxY) 65 | } 66 | 67 | var leftMiddle: CGPoint { 68 | return CGPoint(x: minX, y: midY) 69 | } 70 | 71 | var rightMiddle: CGPoint { 72 | return CGPoint(x: maxX, y: midY) 73 | } 74 | 75 | /// Center taking size into account 76 | var center: CGPoint { 77 | get { 78 | let x = origin.x + size.width / 2 79 | let y = origin.y + size.height / 2 80 | return CGPoint(x: x, y: y) 81 | } 82 | set { 83 | origin.x = newValue.x - size.width / 2 84 | origin.y = newValue.y - size.height / 2 85 | } 86 | } 87 | 88 | var sameCenterSquare: CGRect { 89 | let maxLength = max(size.width, size.height) 90 | var rect = CGRect(x: 0, y: 0, width: maxLength, height: maxLength) 91 | rect.center = center 92 | return rect 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/CoreGraphics/CGSize+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension CGSize { 4 | /// Scales up a point-size CGSize into its pixel representation. 5 | /// 6 | var pixelSize: CGSize { 7 | let scale = UIScreen.main.scale 8 | return CGSize(width: self.width * scale, height: self.height * scale) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/CoreGraphics/Operators.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | public func +(lhs: CGSize, rhs: CGSize) -> CGSize { 4 | return CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height) 5 | } 6 | 7 | public func -(lhs: CGSize, rhs: CGSize) -> CGSize { 8 | return CGSize(width: lhs.width - rhs.width, height: lhs.height - rhs.height) 9 | } 10 | 11 | public func *(lhs: CGSize, rhs: CGFloat) -> CGSize { 12 | return CGSize(width: lhs.width * rhs, height: lhs.height * rhs) 13 | } 14 | 15 | public func /(lhs: CGSize, rhs: CGFloat) -> CGSize { 16 | return CGSize(width: lhs.width / rhs, height: lhs.height / rhs) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/CoreLocation/AuthorizationPublisher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | import CoreLocation 4 | 5 | 6 | protocol PublisherAuthorizationDelegate: AnyObject { 7 | func send(status: CLAuthorizationStatus) 8 | } 9 | 10 | protocol SubscriptionAuthorizationDelegate: AnyObject { 11 | func requestAuthorization(type: CLLocationManager.AuthorizationType) 12 | } 13 | 14 | final class AuthorizationSubscription : NSObject, 15 | PublisherAuthorizationDelegate, 16 | Subscription where S.Input == CLAuthorizationStatus, 17 | S.Failure == Never { 18 | typealias Output = CLAuthorizationStatus 19 | typealias Failure = Never 20 | 21 | var subscriber: S? 22 | private var delegate: SubscriptionAuthorizationDelegate? 23 | private let authorizationType: CLLocationManager.AuthorizationType 24 | 25 | init( 26 | subscriber: S, 27 | authorizationType: CLLocationManager.AuthorizationType, 28 | delegate: SubscriptionAuthorizationDelegate 29 | ) { 30 | self.subscriber = subscriber 31 | self.delegate = delegate 32 | self.authorizationType = authorizationType 33 | } 34 | 35 | deinit { 36 | printDeinitMessage() 37 | } 38 | 39 | func request(_ demand: Subscribers.Demand) { 40 | delegate?.requestAuthorization(type: authorizationType) 41 | } 42 | 43 | func cancel() { 44 | subscriber = nil 45 | delegate = nil 46 | } 47 | 48 | // MARK: - PublisherAuthorizationDelegate 49 | 50 | func send(status: CLAuthorizationStatus) { 51 | _ = subscriber?.receive(status) 52 | } 53 | } 54 | 55 | final class AuthorizationPublisher: NSObject, 56 | Publisher, 57 | CLLocationManagerDelegate, 58 | SubscriptionAuthorizationDelegate { 59 | 60 | typealias Output = CLAuthorizationStatus 61 | typealias Failure = Never 62 | 63 | private let manager: CLLocationManager 64 | private let authorizationType: CLLocationManager.AuthorizationType 65 | private var publisherAuthorizationDelegate: PublisherAuthorizationDelegate? 66 | 67 | init(manager: CLLocationManager, authorizationType: CLLocationManager.AuthorizationType) { 68 | self.manager = manager 69 | self.authorizationType = authorizationType 70 | super.init() 71 | self.manager.delegate = self 72 | } 73 | 74 | deinit { 75 | printDeinitMessage() 76 | } 77 | 78 | func receive(subscriber: S) where S: Subscriber, AuthorizationPublisher.Failure == S.Failure, AuthorizationPublisher.Output == S.Input { 79 | let subscription = AuthorizationSubscription( 80 | subscriber: subscriber, 81 | authorizationType: authorizationType, 82 | delegate: self 83 | ) 84 | subscriber.receive(subscription: subscription) 85 | publisherAuthorizationDelegate = subscription 86 | } 87 | 88 | // MARK: - CLLocationManagerDelegate 89 | 90 | func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 91 | publisherAuthorizationDelegate?.send(status: status) 92 | } 93 | 94 | // MARK: - AuthorizationSubscriptionDelegate 95 | 96 | func requestAuthorization(type: CLLocationManager.AuthorizationType) { 97 | switch type { 98 | case .whenInUse: 99 | manager.requestWhenInUseAuthorization() 100 | case .always: 101 | manager.requestAlwaysAuthorization() 102 | } 103 | } 104 | } 105 | 106 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/CoreLocation/CLLocation.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Combine 3 | 4 | public extension CLLocation { 5 | 6 | /// `CLLocation` from `CLLocationCoordinate2D` 7 | /// - Parameter coordinate: `CLLocationCoordinate2D` 8 | convenience init(from coordinate: CLLocationCoordinate2D) { 9 | self.init(latitude: coordinate.latitude, longitude: coordinate.longitude) 10 | } 11 | 12 | /// Geocode location `Error` 13 | enum GeocodeError: Error { 14 | case invalid(String) 15 | case empty(String) 16 | } 17 | 18 | /// Reverse geocode a `CLLocation` 19 | /// - Parameter location: `CLLocation` 20 | /// - Returns: Future with Result<[`CLPlacemark`], `GeocodeError`> 21 | func reverseGeocode() -> Deferred> { 22 | Deferred { 23 | Future { promise in 24 | CLGeocoder().reverseGeocodeLocation(self) { placemarks, error -> Void in 25 | if let err = error { 26 | return promise(.failure(.invalid("\(String(describing: err))"))) 27 | } 28 | guard let marks = placemarks, !marks.isEmpty else { 29 | return promise(.failure(.empty("\(String(describing: placemarks))"))) 30 | } 31 | return promise(.success(marks)) 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/CoreLocation/CLLocationCoordinate2D.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | 3 | extension CLLocationCoordinate2D: @retroactive Equatable { 4 | 5 | /// Equatable coordinates 6 | public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { 7 | lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude 8 | } 9 | 10 | } 11 | 12 | extension CLLocationCoordinate2D: Codable { 13 | 14 | /// `CLLocationCoordinate2D` at (latitude: 0, longitude 0) 15 | public static let zero = CLLocationCoordinate2D(latitude: 0, longitude: 0) 16 | 17 | /// Encode a `CLLocationCoordinate2D` 18 | /// - Parameter encoder: Encoder 19 | /// - Throws: `EncodingError` 20 | public func encode(to encoder: Encoder) throws { 21 | var container = encoder.unkeyedContainer() 22 | try container.encode(longitude) 23 | try container.encode(latitude) 24 | } 25 | 26 | /// Decode `CLLocationCoordinate2D` from data 27 | /// - Parameter decoder: Decoder 28 | /// - Throws: `DecodingError` 29 | public init(from decoder: Decoder) throws { 30 | var container = try decoder.unkeyedContainer() 31 | let longitude = try container.decode(CLLocationDegrees.self) 32 | let latitude = try container.decode(CLLocationDegrees.self) 33 | self.init(latitude: latitude, longitude: longitude) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/CoreLocation/CLLocationManager.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import CoreLocation 3 | 4 | public extension CLLocationManager { 5 | 6 | /** 7 | ``` 8 | class LocationStore: ObservableObject { 9 | 10 | @Published var status: CLAuthorizationStatus = .notDetermined 11 | let manager = CLLocationManager() 12 | var cancellables = Set() 13 | 14 | func requestPermission() { 15 | manager 16 | .requestLocationWhenInUseAuthorization() 17 | .assign(to: &$status) 18 | } 19 | } 20 | ``` 21 | */ 22 | /// Request locaton authorization and subscribe to `CLAuthorizationStatus` updates 23 | /// - Returns: Publisher with CLAuthorizationStatus 24 | func requestLocationWhenInUseAuthorization() -> AnyPublisher { 25 | AuthorizationPublisher(manager: self, authorizationType: .whenInUse) 26 | .eraseToAnyPublisher() 27 | } 28 | 29 | /** 30 | ``` 31 | class LocationStore: ObservableObject { 32 | 33 | @Published var status: CLAuthorizationStatus = .notDetermined 34 | let manager = CLLocationManager() 35 | var cancellables = Set() 36 | 37 | func requestPermission() { 38 | manager 39 | .requestLocationWhenInUseAuthorization() 40 | .assign(to: &$status) 41 | } 42 | } 43 | ``` 44 | */ 45 | /// Request locaton **always** authorization `CLAuthorizationStatus` with **upgrade** prompt 46 | /// - Returns: Publisher with CLAuthorizationStatus 47 | func requestLocationAlwaysAuthorization() -> AnyPublisher { 48 | AuthorizationPublisher(manager: self, authorizationType: .whenInUse) 49 | .flatMap { status -> AnyPublisher in 50 | if status == CLAuthorizationStatus.authorizedWhenInUse { 51 | return AuthorizationPublisher( 52 | manager: self, 53 | authorizationType: .always 54 | ) 55 | .eraseToAnyPublisher() 56 | } 57 | return Just(status).eraseToAnyPublisher() 58 | } 59 | .eraseToAnyPublisher() 60 | } 61 | 62 | /** 63 | ``` 64 | class LocationStore: ObservableObject { 65 | 66 | @Published var coordinate: CLLocationCoordinate2D = .zero 67 | let manager = CLLocationManager() 68 | var cancellables = Set() 69 | 70 | func requestLocation() { 71 | manager 72 | .receiveLocationUpdates() 73 | .compactMap(\.last) 74 | .map(\.coordinate) 75 | .replaceError(with: .zero) 76 | .assign(to: &$coordinate) 77 | } 78 | } 79 | ``` 80 | */ 81 | /// Receive location updates from the `CLLocationManager` 82 | /// - Parameter oneTime: One time location update or constant updates, default: false 83 | /// - Returns: Publisher with [CLLocation] or Error 84 | func receiveLocationUpdates(oneTime: Bool = false) -> AnyPublisher<[CLLocation], Error> { 85 | LocationPublisher(manager: self, onTimeUpdate: oneTime) 86 | .eraseToAnyPublisher() 87 | } 88 | 89 | /// Authorization access level 90 | internal enum AuthorizationType: String { 91 | case always, whenInUse 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/CoreLocation/LocationPublisher.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import CoreLocation 3 | 4 | protocol PublisherLocationDelegate: AnyObject { 5 | func startLocationUpdates() 6 | } 7 | 8 | protocol SubscriptionLocationDelegate: AnyObject { 9 | func didUpdate(with locations: [CLLocation]) 10 | func didFail(with error: Error) 11 | } 12 | 13 | final class LocationSubscription : 14 | NSObject, 15 | SubscriptionLocationDelegate, 16 | Subscription where S.Input == [CLLocation], 17 | S.Failure == Error { 18 | 19 | var subscriber: S? 20 | private var publisherDelegate: PublisherLocationDelegate? 21 | 22 | init(subscriber: S?, delegate: PublisherLocationDelegate?) { 23 | self.subscriber = subscriber 24 | self.publisherDelegate = delegate 25 | } 26 | 27 | func didUpdate(with locations: [CLLocation]) { 28 | _ = subscriber?.receive(locations) 29 | } 30 | 31 | func didFail(with error: Error) { 32 | _ = subscriber?.receive(completion: .failure(error)) 33 | } 34 | 35 | func request(_ demand: Subscribers.Demand) { 36 | publisherDelegate?.startLocationUpdates() 37 | } 38 | 39 | deinit { 40 | printDeinitMessage() 41 | } 42 | 43 | func cancel() { 44 | subscriber = nil 45 | publisherDelegate = nil 46 | } 47 | 48 | } 49 | 50 | final class LocationPublisher: NSObject, 51 | Publisher, 52 | CLLocationManagerDelegate, 53 | PublisherLocationDelegate { 54 | typealias Output = [CLLocation] 55 | typealias Failure = Error 56 | 57 | private let manager: CLLocationManager 58 | private let oneTimeUpdate: Bool 59 | private var subscriberDelegate: SubscriptionLocationDelegate? 60 | 61 | init(manager: CLLocationManager, onTimeUpdate: Bool = false) { 62 | self.manager = manager 63 | self.oneTimeUpdate = onTimeUpdate 64 | super.init() 65 | self.manager.delegate = self 66 | } 67 | 68 | deinit { 69 | printDeinitMessage() 70 | } 71 | 72 | // MARK: Publisher 73 | 74 | func receive(subscriber: S) where S : Subscriber, LocationPublisher.Failure == S.Failure, LocationPublisher.Output == S.Input { 75 | let subscibtion = LocationSubscription( 76 | subscriber: subscriber, 77 | delegate: self 78 | ) 79 | subscriber.receive(subscription: subscibtion) 80 | subscriberDelegate = subscibtion 81 | } 82 | 83 | // MARK: CLLocationManagerDelegate 84 | 85 | func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 86 | subscriberDelegate?.didFail(with: error) 87 | manager.stopUpdatingLocation() 88 | } 89 | 90 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 91 | subscriberDelegate?.didUpdate(with: locations) 92 | } 93 | 94 | // MARK: PublisherLocationDelegate 95 | 96 | func startLocationUpdates() { 97 | if oneTimeUpdate { 98 | manager.requestLocation() 99 | } else { 100 | manager.startUpdatingLocation() 101 | } 102 | } 103 | } 104 | 105 | 106 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/Apply.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Apply.swift 3 | // ExtensionKit 4 | // 5 | // Created by Gary Tokman on 9/22/24. 6 | // 7 | 8 | import Foundation.NSJSONSerialization 9 | import Foundation.NSObject 10 | 11 | public protocol Apply {} 12 | 13 | public extension Apply where Self: Any { 14 | /// Makes it available to set properties with closures just after initializing. 15 | /// 16 | /// let label = UILabel().apply { 17 | /// $0.textAlignment = .center 18 | /// $0.textColor = .black 19 | /// $0.text = "Hello, World!" 20 | /// } 21 | @discardableResult 22 | func apply(_ block: (Self) -> Void) -> Self { 23 | // https://github.com/devxoul/Then 24 | block(self) 25 | return self 26 | } 27 | } 28 | 29 | extension NSObject: Apply {} 30 | extension JSONDecoder: Apply {} 31 | extension JSONEncoder: Apply {} 32 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/Array.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Array where Element: Equatable { 4 | 5 | /// Removes the given element in the array. 6 | /// 7 | /// - Parameter element: The element to be removed. 8 | /// - Returns: The element got removed, or `nil` if the element doesn't exist. 9 | @discardableResult 10 | mutating func remove(_ element: Element) -> Element? { 11 | if let index = self.firstIndex(of: element) { 12 | return self.remove(at: index) 13 | } 14 | return nil 15 | } 16 | 17 | /// Returns an array where repeating elements of the receiver got removed. 18 | var removingRepeatElements: Array { 19 | var arr = Array() 20 | forEach { 21 | if !arr.contains($0) { 22 | arr.append($0) 23 | } 24 | } 25 | return arr 26 | } 27 | } 28 | 29 | 30 | public extension Array where Element == URLQueryItem { 31 | 32 | /// Initialize `URLQueryItem from dictionary` 33 | /// - Parameter dictionary: url parameters 34 | init(_ dictionary: [String: T]) { 35 | self = dictionary.map { (key, value) -> Element in 36 | .init(name: key, value: String(value)) 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/Bundle.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Combine 3 | 4 | public extension Bundle { 5 | 6 | /// Version number via - CFBundleShortVersionString 7 | var versionNumber: String { 8 | infoDictionary?["CFBundleShortVersionString"] as? String ?? "" 9 | } 10 | 11 | /// Build number via - CFBundleVersion 12 | var buildNumber: String { 13 | infoDictionary?["CFBundleVersion"] as? String ?? "" 14 | } 15 | 16 | internal enum DataDecodingError: Error { 17 | case urlNotFound(String) 18 | case contentsOfURL(String) 19 | } 20 | 21 | /// Load model type from bundle resource 22 | /// - Parameters: 23 | /// - type: Type to load 24 | /// - file: File name 25 | /// - dateDecodingStrategy: date decoding strategy 26 | /// - keyDecodingStrategy: key decoding strategy 27 | /// - Returns: Future 28 | func decode( 29 | _ type: T.Type, 30 | fromFile file: String, 31 | withExtension `extension`: String? = nil, 32 | dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate, 33 | keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys 34 | ) -> Deferred> { 35 | Deferred { 36 | Future { promise in 37 | guard let url = self.url(forResource: file, withExtension: `extension`) else { 38 | promise(.failure(DataDecodingError.urlNotFound("Failed to locate \(file) in bundle."))) 39 | return 40 | } 41 | 42 | guard let data = try? Data(contentsOf: url) else { 43 | promise(.failure(DataDecodingError.contentsOfURL("Failed to load \(file) in bundle."))) 44 | return 45 | } 46 | 47 | let decoder = JSONDecoder() 48 | decoder.dateDecodingStrategy = dateDecodingStrategy 49 | decoder.keyDecodingStrategy = keyDecodingStrategy 50 | 51 | do { 52 | let type = try decoder.decode(T.self, from: data) 53 | promise(.success(type)) 54 | } catch { 55 | promise(.failure(error)) 56 | } 57 | 58 | } 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/CFRunLoopTimer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension CFRunLoopTimer { 4 | 5 | /// Invalidate CFRunLoopTimer 6 | func invalidate() { 7 | CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), self, .commonModes) 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/Calendar.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | public extension Calendar.Component { 5 | 6 | /// All Calendar Components 7 | static let allCases: Set = [.year, .month, .day, .hour, .minute, .second, .weekday, .weekdayOrdinal, .weekOfYear] 8 | } 9 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/Collection.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Collection { 4 | 5 | /// Safe indexing. 6 | /// Returns the element at the specified index iff it is within bounds, otherwise nil. 7 | /// ref: https://stackoverflow.com/questions/25329186/safe-bounds-checked-array-lookup-in-swift-through-optional-bindings 8 | /// 9 | /// - Parameter index: The index used to retrieve a value / an object. 10 | subscript (safe index: Index) -> Element? { 11 | return indices.contains(index) ? self[index] : nil 12 | } 13 | 14 | /// Distance to index 15 | /// - Parameter index: Index 16 | /// - Returns: Int 17 | func distance(to index: Index) -> Int { 18 | return self.distance(from: startIndex, to: index) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/Data.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Data { 4 | /// Returns a string of hex value. 5 | var hexString: String { 6 | return withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> String in 7 | let buffer = bytes.bindMemory(to: UInt8.self) 8 | return buffer.map {String(format: "%02hhx", $0)}.reduce("", { $0 + $1 }) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/Date.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Date { 4 | 5 | /// Days of the week in Gregorian calendar (Sunday - Saturday) 6 | static let days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] 7 | 8 | /// Returns the year from the date. 9 | var year: Int { Calendar.current.component(.year, from: self) } 10 | 11 | /// Returns month as `Int` starting from `1...12`. 12 | var month: Int { Calendar.current.component(.month, from: self) } 13 | 14 | /// Returns week as `Int` starting from 1...52 15 | var week: Int { Calendar.current.component(.weekOfYear, from: self) } 16 | 17 | var weekday: Int { Calendar.current.component(.weekday, from: self) } 18 | 19 | var weekOfMonth: Int { Calendar.current.component(.weekOfMonth, from: self) } 20 | 21 | var day: Int { Calendar.current.component(.day, from: self) } 22 | 23 | var hour: Int { Calendar.current.component(.hour, from: self) } 24 | 25 | var minute: Int { Calendar.current.component(.minute, from: self) } 26 | 27 | var second: Int { Calendar.current.component(.second, from: self) } 28 | 29 | var nanos: Int { Calendar.current.component(.nanosecond, from: self) } 30 | 31 | var yesterday: Date { self.adjust(.day, offset: -1) } 32 | 33 | var today: Date { self.startOfDay } 34 | 35 | var tomorrow: Date { self.adjust(.day, offset: 1) } 36 | 37 | var dayAfter: Date { self.adjust(.day, offset: 2) } 38 | 39 | var dayBefore: Date { self.adjust(.day, offset: -2) } 40 | 41 | var isLastDayOfMonth: Bool { return self.tomorrow.month != month } 42 | 43 | /// Offset component by amount 44 | /// - Parameters: 45 | /// - type: Component 46 | /// - offset: Offset to add 47 | /// - Returns: Date 48 | func adjust(_ type: Calendar.Component, offset: Int) -> Date { 49 | var comp = DateComponents() 50 | comp.setValue(offset, for: type) 51 | return Calendar.current.date(byAdding: comp, to: self)! 52 | } 53 | 54 | /// Start of current day 55 | var startOfDay: Date { 56 | return Calendar.current.startOfDay(for: self) 57 | } 58 | 59 | /// End of current day 60 | var endOfDay: Date { 61 | let cal = Calendar.current 62 | var comp = DateComponents() 63 | comp.day = 1 64 | return cal.date(byAdding: comp, to: self.startOfDay)!.addingTimeInterval(-1) 65 | } 66 | 67 | /// Get relative String back from Date ex: 1 year ago, 1 month ago ... 68 | /// - Parameters: 69 | /// - currentDate: Current Date 70 | /// - numericDates: Display the numeric value in string ex: 1 year ago vs Last year 71 | /// - Returns: String 72 | func toRelativeFormat( 73 | currentDate: Date = Date(), 74 | numericDates: Bool = true 75 | ) -> String { 76 | let calendar = Calendar.current 77 | let now = currentDate 78 | let earliest = (now as NSDate).earlierDate(self) 79 | let latest = (earliest == now) ? self : now 80 | let components = calendar.dateComponents( 81 | Calendar.Component.allCases, from: earliest, to: latest) 82 | 83 | if let year = components.year, year >= 2 { 84 | return "\(year) years ago" 85 | } else if let year = components.year, year >= 1 { 86 | if (numericDates) { 87 | return "1 year ago" 88 | } else { 89 | return "Last year" 90 | } 91 | } else if let month = components.month, month >= 2 { 92 | return "\(month) months ago" 93 | } else if let month = components.month, month >= 1 { 94 | if (numericDates) { 95 | return "1 month ago" 96 | } else { 97 | return "Last month" 98 | } 99 | } else if let week = components.weekOfYear, week >= 2 { 100 | return "\(week) weeks ago" 101 | } else if let week = components.weekOfYear, week >= 1 { 102 | if (numericDates) { 103 | return "1 week ago" 104 | } else { 105 | return "Last week" 106 | } 107 | } else if let day = components.day, day >= 2 { 108 | return "\(day) days ago" 109 | } else if let day = components.day, day >= 1 { 110 | if (numericDates) { 111 | return "1 day ago" 112 | } else { 113 | return "Yesterday" 114 | } 115 | } else if let hour = components.hour, hour >= 2 { 116 | return "\(hour) hours ago" 117 | } else if let hour = components.hour, hour >= 1 { 118 | if (numericDates) { 119 | return "1 hour ago" 120 | } else { 121 | return "An hour ago" 122 | } 123 | } else if let min = components.minute, min >= 2 { 124 | return "\(min) minutes ago" 125 | } else if let min = components.minute, min >= 1 { 126 | if (numericDates) { 127 | return "1 minute ago" 128 | } else { 129 | return "A minute ago" 130 | } 131 | } else if let sec = components.second, sec >= 3 { 132 | return "\(sec) seconds ago" 133 | } else { 134 | return "Just now" 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/DispactQueue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension DispatchQueue { 4 | 5 | /// Run closure if thread is main else switch to main and run the closure 6 | /// - Parameter work: Closure to run 7 | static func mainSafeAsync(execute work: @escaping () -> Void) { 8 | if Thread.isMainThread { 9 | work() 10 | } else { 11 | main.async(execute: work) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/FileManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension FileManager { 4 | 5 | /// Size of file at path 6 | /// 7 | /// - Parameter path: file path 8 | /// - Returns: Size in bytes 9 | func fileSize(atPath path: String) -> Int { 10 | let attributes = try? attributesOfItem(atPath: path) 11 | return (attributes?[FileAttributeKey.size] as? Int) ?? 0 12 | } 13 | 14 | /// Size of folder 15 | /// 16 | /// - Parameter path: folder path 17 | /// - Returns: size in bytes 18 | func folderSize(atPath path: String) -> Int { 19 | let manager = FileManager.default 20 | if manager.fileExists(atPath: path) { 21 | do { 22 | let childFilesEnumerator = try (manager.subpathsOfDirectory(atPath: path) as NSArray).objectEnumerator() 23 | var folderSize = 0 24 | while childFilesEnumerator.nextObject() != nil { 25 | if let fileName = childFilesEnumerator.nextObject() as? String, 26 | let url = URL(string: path) { 27 | let fileAbsolutePath = url.appendingPathComponent(fileName).absoluteString 28 | folderSize += self.fileSize(atPath: fileAbsolutePath) 29 | } 30 | } 31 | return folderSize 32 | 33 | } catch { 34 | dprint(error) 35 | } 36 | } 37 | return 0 38 | } 39 | 40 | 41 | /// Size of directory at URL 42 | /// 43 | /// - Parameter URL: URL 44 | /// - Returns: Size in bytes 45 | func directorySize(at URL: URL) -> Int { 46 | var result = 0 47 | let properties = [URLResourceKey.localizedNameKey, URLResourceKey.creationDateKey, URLResourceKey.localizedTypeDescriptionKey] 48 | let manager = FileManager.default 49 | do { 50 | let urls = try manager.contentsOfDirectory(at: URL, includingPropertiesForKeys: properties, options: .skipsHiddenFiles) 51 | for fileSystemItem in urls { 52 | var directory: ObjCBool = false 53 | let path = fileSystemItem.path 54 | manager.fileExists(atPath: path, isDirectory: &directory) 55 | if directory.boolValue { 56 | result += directorySize(at: fileSystemItem) 57 | } else { 58 | result += try manager.attributesOfItem(atPath: path)[FileAttributeKey.size] as! Int 59 | } 60 | } 61 | 62 | } catch { 63 | dprint("Error: \(error)") 64 | } 65 | return result 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/Global.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// AppVersion set in `CFBundleShortVersionString` 4 | public var appVersion: String { 5 | return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String 6 | } 7 | 8 | /// Languages set in user defaults key `AppleLanguages` 9 | public var systemLanguages: [String] { 10 | return UserDefaults.standard.stringArray(forKey: "AppleLanguages") ?? [] 11 | } 12 | 13 | /// Sleeps the thread 14 | /// - Parameter duration: in seconds 15 | public func sleep(duration: TimeInterval) { 16 | #if DEBUG 17 | Thread.sleep(forTimeInterval: duration) 18 | #endif 19 | } 20 | 21 | /// Swift still calls `print()` and/or `debugPrint()` in shipped apps. 22 | /// We use a method described in onevcat's post (https://onevcat.com/2016/02/swift-performance/) 23 | /// to optimaze the performance. 24 | /// 25 | /// - Parameter item: items to print 26 | public func dprint(_ item: @autoclosure () -> Any, _ event: PrintEvent = .d) { 27 | #if DEBUG 28 | print("\(event.rawValue): \(item())") 29 | #endif 30 | } 31 | 32 | /// Print event type 33 | public enum PrintEvent: String { 34 | /// Error 35 | case e = "[‼️]" // error 36 | /// Info 37 | case i = "[ℹ️]" // info 38 | /// Debug 39 | case d = "[💬]" // debug 40 | /// Verbose 41 | case v = "[🔬]" // verbose 42 | /// Warning 43 | case w = "[⚠️]" // warning 44 | 45 | var value: String { 46 | get { 47 | return self.rawValue; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/Int.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Int { 4 | 5 | /// Whether self is an odd number 6 | var isOdd: Bool { 7 | return !isEven 8 | } 9 | 10 | /// Whether self is an even number 11 | var isEven: Bool { 12 | return self % 2 == 0 13 | } 14 | 15 | /// Treats 0 as nil 16 | var nilIfZero: Int? { 17 | if self == 0 { return nil } 18 | return self 19 | } 20 | 21 | /// Make the number to string 22 | var string: String { 23 | return String(self) 24 | } 25 | 26 | /// Make a range from zero to self 27 | var range: CountableRange { 28 | return 0..(of creation: @autoclosure () throws -> T) rethrows -> [T] { 58 | return try (0 ..< self).map { _ in 59 | try creation() 60 | } 61 | } 62 | 63 | /// Return if `self` is in the given range. 64 | /// 65 | /// - Parameter range: Target range. 66 | /// - Returns: `true` if self is in the range, otherwise `false`. 67 | func inRange(_ range: Range) -> Bool { 68 | return range.contains(self) 69 | } 70 | 71 | /// Calls the given block n number of times. 72 | func times(block: () -> Void) { 73 | if self <= 0 { return } 74 | for _ in 0.. Array<(offset: Int, element: Element)> { 8 | Array(enumerated()) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/String.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import UIKit 4 | 5 | #endif 6 | 7 | internal extension StaticString { 8 | var toString: String { 9 | "\(self)" 10 | } 11 | } 12 | 13 | public extension String { 14 | 15 | subscript(_ i: Int) -> String { 16 | let idx1 = index(startIndex, offsetBy: i) 17 | let idx2 = index(idx1, offsetBy: 1) 18 | return String(self[idx1..) -> String { 22 | let start = index(startIndex, offsetBy: r.lowerBound) 23 | let end = index(startIndex, offsetBy: r.upperBound) 24 | return String(self[start ..< end]) 25 | } 26 | 27 | subscript(r: CountableClosedRange) -> String { 28 | let startIndex = self.index(self.startIndex, offsetBy: r.lowerBound) 29 | let endIndex = self.index(startIndex, offsetBy: r.upperBound - r.lowerBound) 30 | return String(self[startIndex...endIndex]) 31 | } 32 | 33 | /// Remove HTML tags from String 34 | var removeHTML: String { 35 | return self.replacingOccurrences( 36 | of: "<[^>]+>", with: "", options: .regularExpression, range: nil 37 | ) 38 | } 39 | 40 | /// Is valid email 41 | var isEmail: Bool { 42 | let emailRegax = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}" 43 | let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegax) 44 | return emailTest.evaluate(with: self) 45 | } 46 | 47 | /// Are numbers 0-9 48 | var areNumbers: Bool { 49 | let allnumRegex = "^[0-9]*$" 50 | let numPredicate = NSPredicate(format: "SELF MATCHES %@", allnumRegex) 51 | return numPredicate.evaluate(with: self) 52 | } 53 | 54 | /// Cast to Int 55 | var toInt: Int? { Int(self) } 56 | 57 | /// Cast to Double 58 | var toDouble: Double? { Double(self) } 59 | 60 | /// Trimming ".0" 61 | var trimmingZeroDecimal: String { 62 | if let double = Double(self), double.truncatingRemainder(dividingBy: 1) == 0 { 63 | return String(format: "%.0f", double) 64 | } 65 | return self 66 | } 67 | 68 | /// Trim white space and new lines 69 | func trim() -> String { 70 | return self.trimmingCharacters(in: .whitespacesAndNewlines) 71 | } 72 | 73 | /// Adding "+" at the very beginning. 74 | var addingPlusSymbol: String { "+" + self } 75 | 76 | /// Adding "-" at the very beginning. 77 | var addingMinusSymbol: String { "-" + self } 78 | 79 | /// Returns the CGSize that the string being layout on screen. 80 | /// 81 | /// - Parameter font: The given font. 82 | /// - Returns: The result CGSize. 83 | func layoutSize(with font: UIFont) -> CGSize { 84 | self.size(withAttributes: [.font: font]) 85 | } 86 | 87 | /// Cast as Int and add the given value. No changes if casting fails. 88 | mutating func advanceNumberValue(step: Int = 1) { 89 | if let value = Int(self) { 90 | self = String(value.advanced(by: step)) 91 | } 92 | } 93 | 94 | /// Comparing app versions. Returns `true` if self is `1.1.0` and the given value is `1.2.0`. 95 | /// - Parameter aVersion: Another version. 96 | /// - Returns: `true` if the give version is newer than self. 97 | func isOldAppVersion(comparedWith aVersion: String) -> Bool { 98 | self.compare(aVersion, options: .numeric) == .orderedAscending 99 | } 100 | 101 | var uppercasingFirstLetter: String { 102 | prefix(1).uppercased() + dropFirst() 103 | } 104 | 105 | var lowercasingFirstLetter: String { 106 | prefix(1).lowercased() + dropFirst() 107 | } 108 | 109 | /// Return `true` if self is empty or only contains white spaces and/or new lines. 110 | var isBlank: Bool { isEmpty || trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } 111 | 112 | /// Return `false` if self is empty or only contains white spaces and/or new lines. 113 | var isVisible: Bool { !isBlank } 114 | 115 | /// Return `nil` if `self.isBlank` is `true`. 116 | var nilIfBlank: String? { isBlank ? nil : self } 117 | 118 | @available(*, deprecated, message: "Use `nilIfBlank` instead.") 119 | func treatsVisuallyEmptyAsNil() -> String? { nilIfBlank } 120 | 121 | /// Get subscring from created range 122 | /// - Parameters: 123 | /// - from: from String 124 | /// - to: to String 125 | /// - Returns: String 126 | func slice(from: String, to: String) -> String? { 127 | if let r1 = self.range(of: from)?.upperBound, let r2 = self.range(of: to)?.lowerBound { 128 | return (String(self[r1..(in string: S) -> Int { return string.distance(to: self) } 145 | } 146 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/Timer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Timer { 4 | 5 | /// Schedule closure to run on main run loop after delay 6 | /// 7 | /// - Parameters: 8 | /// - delay: Delay interval 9 | /// - handler: Closure to run 10 | /// - Returns: `CFRunLoopTimer` 11 | @discardableResult 12 | class func schedule(delay: TimeInterval, handler: @escaping () -> Void) -> CFRunLoopTimer? { 13 | let fireDate = delay + CFAbsoluteTimeGetCurrent() 14 | let timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, fireDate, 0, 0, 0) { _ in 15 | handler() 16 | } 17 | CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, CFRunLoopMode.commonModes) 18 | return timer 19 | } 20 | 21 | 22 | /// Schedule closure to run on main run loop and repeat at the interval 23 | /// 24 | /// - Parameters: 25 | /// - interval: Interval 26 | /// - handler: Closure to run 27 | /// - Returns: CFRunLoopTimer 28 | @discardableResult 29 | class func schedule(repeatInterval interval: TimeInterval, handler: @escaping () -> Void) -> CFRunLoopTimer? { 30 | let fireDate = interval + CFAbsoluteTimeGetCurrent() 31 | let timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, fireDate, interval, 0, 0) { _ in 32 | handler() 33 | } 34 | CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, CFRunLoopMode.commonModes) 35 | return timer 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/URL.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension URL { 4 | 5 | /// Create URL from String 6 | /// - Parameter string: URL string 7 | init(_ string: StaticString) { 8 | self.init(string: "\(string)")! 9 | } 10 | 11 | /* 12 | ``` 13 | static let url = URL( 14 | "https://api.instagram.com", 15 | path: "/oauth/authorize", 16 | parameters: Plist.authorize as! [String: String] 17 | )! 18 | ``` 19 | **/ 20 | /// Create URL from baseUrl, path, and parameters 21 | /// - Parameters: 22 | /// - baseUrl: base URL including the host (https) 23 | /// - path: url path default, empty string 24 | /// - parameters: parameters, key/value pair 25 | init?( 26 | _ baseUrl: StaticString, 27 | path: StaticString = "", 28 | parameters: [String: T] = [:] 29 | ) { 30 | var urlComponents = URLComponents(string: baseUrl.toString) 31 | if !path.toString.isEmpty { 32 | urlComponents?.path = path.toString 33 | } 34 | urlComponents?.queryItems = .init(parameters) 35 | guard let url = urlComponents?.url else { 36 | dprint("ERROR: Could not create URL from \(baseUrl) + \(path) + \(parameters)") 37 | return nil 38 | } 39 | self = url 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Foundation/UserDefaults.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension UserDefaults { 4 | 5 | /// Get `Codable` model from user defaults 6 | /// - Parameter key: String key 7 | /// - Returns: Model 8 | func getCodable(forKey key: String) -> T? { 9 | guard let data = UserDefaults.standard.data(forKey: key) else { 10 | return nil 11 | } 12 | let element = try? JSONDecoder().decode(T.self, from: data) 13 | return element 14 | } 15 | 16 | /// Set `Codable` mode to user defaults 17 | /// - Parameters: 18 | /// - value: Model 19 | /// - key: String key 20 | func setCodable(value: T, forKey key: String) { 21 | let data = try? JSONEncoder().encode(value) 22 | UserDefaults.standard.setValue(data, forKey: key) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/PropertyWrappers/UserDefault.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /** 4 | ``` 5 | @UserDefault("key") var myInt = 0 6 | ``` 7 | */ 8 | /// Save Objects in UserDefaults.standard with Property wrapper syntax 9 | @propertyWrapper 10 | public struct UserDefault { 11 | 12 | /// Key 13 | public let key: String 14 | 15 | /// Get the value if it existes 16 | public var wrappedValue: T? { 17 | get { return UserDefaults.standard.object(forKey: key) as? T } 18 | set { UserDefaults.standard.set(newValue, forKey: key) } 19 | } 20 | 21 | /// Create a new Default with Key 22 | /// - Parameter key: String key 23 | public init(_ key: String) { self.key = key } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/Array+View.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | 4 | extension Array: @retroactive View where Element: View { 5 | 6 | /* 7 | ``` 8 | struct SwiftUIView: View { 9 | let array = ["hello", "world"] 10 | 11 | var body: some View { 12 | VStack { 13 | array.map(Text.init) 14 | } 15 | } 16 | } 17 | ``` 18 | */ 19 | /// Add View conformance to Array where element is View 20 | public var body: some View { 21 | ForEach(indices, id: \.self) { self[$0] } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/Binding.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension Binding where Value: Equatable { 4 | 5 | /** 6 | ``` 7 | Toggle( 8 | "Foo", 9 | isOn: $foo.onChange { 10 | bar.isEnabled = $0 11 | } 12 | ) 13 | ``` 14 | */ 15 | 16 | /// Update or run action after binding has changed 17 | /// - Parameter action: Action to run 18 | /// - Returns: Binding 19 | func onChange(_ action: @escaping (Value) -> Void) -> Self { 20 | .init( 21 | get: { wrappedValue }, 22 | set: { 23 | let oldValue = wrappedValue 24 | wrappedValue = $0 25 | let newValue = wrappedValue 26 | if newValue != oldValue { 27 | action(newValue) 28 | } 29 | } 30 | ) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/Button.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension Button where Label == Image { 4 | 5 | /// Initialize button with system image and trailing action 6 | /// - Parameters: 7 | /// - systemName: System image name 8 | /// - action: action 9 | init(systemName: String, action: @escaping () -> Void) { 10 | self.init(action: action) { 11 | Image(systemName: systemName) 12 | } 13 | } 14 | 15 | /// Initialize button with local image and trailing action 16 | /// - Parameters: 17 | /// - systemName: System image name 18 | /// - action: action 19 | init(imageName: String, action: @escaping () -> Void) { 20 | self.init(action: action) { 21 | Image(imageName) 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/Color.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKit 3 | 4 | @available(iOS 13.0, macOS 10.15, *) 5 | public extension Color { 6 | 7 | /// Initialize with HEX 8 | /// - Parameter hex: HEX UInt value 9 | /// - Returns: Color 10 | static func hex(_ hex: UInt, alpha: Double = 1) -> Self { 11 | Self( 12 | red: Double((hex & 0xff0000) >> 16) / 255, 13 | green: Double((hex & 0x00ff00) >> 8) / 255, 14 | blue: Double(hex & 0x0000ff) / 255, 15 | opacity: alpha 16 | ) 17 | } 18 | 19 | /// Initialize with HEX 20 | /// - Parameter hex: HEX String value 21 | /// - Returns: Color 22 | static func hex(_ hex: String, alpha: CGFloat = 1) -> Self { 23 | self.init(UIColor(hexCode: hex, alpha: alpha)) 24 | } 25 | 26 | /// Initialize with HEX 27 | /// - Parameter hex: HEX 28 | init(hex: String) { 29 | self.init(UIColor(hexCode: hex)) 30 | } 31 | 32 | /// Generate a random color 33 | /// - Parameter opacity: opacity 34 | /// - Returns: New color 35 | static func random(opacity: CGFloat = 1.0) -> Color{ 36 | return Color(.random(alpha: opacity)) 37 | } 38 | 39 | /// Lighten color 40 | /// - Parameter percentage: percentage (1 -100), default: 30 41 | /// - Returns: new color 42 | @available(iOS 14.0, *) 43 | func lighten(by percentage: CGFloat = 30.0) -> Color { 44 | let uiColor = UIColor(self) 45 | let adjustedColor = uiColor.lighten(by: percentage) 46 | return Color(adjustedColor) 47 | } 48 | 49 | /// Darken color 50 | /// - Parameter percentage: percentage (1 -100), default: 30 51 | /// - Returns: new color 52 | @available(iOS 14.0, *) 53 | func darken(by percentage: CGFloat = 30.0) -> Color { 54 | let uiColor = UIColor(self) 55 | let adjustedColor = uiColor.darken(by: percentage) 56 | return Color(adjustedColor) 57 | } 58 | 59 | @available(iOS 14.0, *) 60 | /// Inverse color 61 | var inverted: Color { 62 | var a: CGFloat = 0.0, r: CGFloat = 0.0, g: CGFloat = 0.0, b: CGFloat = 0.0 63 | return UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) 64 | ? Color(UIColor(red: 1.0-r, green: 1.0-g, blue: 1.0-b, alpha: a)) 65 | : .black 66 | } 67 | 68 | /// Create a color with a dark and light mode UIColor 69 | /// - Parameters: 70 | /// - light: light color 71 | /// - dark: dark color 72 | /// - Returns: Color 73 | static func dynamicColor(light: UIColor, dark: UIColor) -> Color { 74 | self.init(UIColor.dynamicColor(light: light, dark: dark)) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/Geometry/GeometryProxy.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @available(iOS 13.0, OSX 10.15, *) 4 | public extension GeometryProxy { 5 | 6 | /// Returns the width minus the safeAreaInsets. 7 | var safeWidth: CGFloat { 8 | size.width - safeAreaInsets.leading - safeAreaInsets.trailing 9 | } 10 | 11 | /// Returns the height minus the safeAreaInsets. 12 | var safeHeight: CGFloat { 13 | size.height - safeAreaInsets.top - safeAreaInsets.bottom 14 | } 15 | 16 | var localFrame: CGRect { frame(in: .local) } 17 | 18 | var localWidth: CGFloat { localFrame.width } 19 | 20 | var localHeight: CGFloat { localFrame.height } 21 | 22 | var localCenter: CGPoint { .init(x: localFrame.midX, y: localFrame.midY) } 23 | 24 | var localTop: CGFloat { localFrame.minY } 25 | 26 | var localBottom: CGFloat { localFrame.maxY } 27 | 28 | var localLeft: CGFloat { localFrame.minX } 29 | 30 | var localRight: CGFloat { localFrame.maxX } 31 | 32 | var localDiameter: CGFloat { return min(localWidth, localHeight) } 33 | 34 | var globalFrame: CGRect { frame(in: .global) } 35 | 36 | var globalWidth: CGFloat { globalFrame.width } 37 | 38 | var globalHeight: CGFloat { globalFrame.height } 39 | 40 | var globalCenter: CGPoint { .init(x: globalFrame.midX, y: globalFrame.midY) } 41 | 42 | var globalTop: CGFloat { globalFrame.minY } 43 | 44 | var globalBottom: CGFloat { globalFrame.maxY } 45 | 46 | var globalLeft: CGFloat { globalFrame.minX } 47 | 48 | var globalRight: CGFloat { globalFrame.maxX } 49 | 50 | var globalDiameter: CGFloat { return min(globalWidth, globalHeight) } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/Geometry/RectGetter.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// Get the views frame in the global coordinate space 4 | struct RectGetter: View { 5 | 6 | @Binding var rect: CGRect 7 | 8 | var body: some View { 9 | GeometryReader { proxy in 10 | self.createView(proxy: proxy) 11 | } 12 | } 13 | 14 | func createView(proxy: GeometryProxy) -> some View { 15 | DispatchQueue.mainSafeAsync { 16 | self.rect = proxy.globalFrame 17 | } 18 | 19 | return Rectangle().fill(Color.clear) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/Image.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension Image { 4 | 5 | /// Initialize with a default image 6 | /// - Parameters: 7 | /// - name: primary image name 8 | /// - defaultImage: default image name 9 | init(_ name: String, defaultImage: String) { 10 | if let img = UIImage(named: name) { 11 | self.init(uiImage: img) 12 | } else { 13 | self.init(defaultImage) 14 | } 15 | } 16 | 17 | /// Initialize with default system image 18 | /// - Parameters: 19 | /// - name: primary image name 20 | /// - defaultSystemImage: default image name 21 | init(_ name: String, defaultSystemImage: String) { 22 | if let img = UIImage(named: name) { 23 | self.init(uiImage: img) 24 | } else { 25 | self.init(systemName: defaultSystemImage) 26 | } 27 | } 28 | 29 | /// Create a resizable image with CGSize 30 | /// - Parameter size: CGSize 31 | /// - Returns: View 32 | func icon(with size: CGSize) -> some View { 33 | self.renderingMode(.template) 34 | .resizable() 35 | .frame(width: size.width, height: size.height) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/Keyboard/KeyboardInfo.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Struct modeling keyboard updates 4 | public struct KeyboardInfo: Equatable { 5 | /// Keyboard height 6 | public var height: CGFloat = 0 7 | /// Keyboard animation curve 8 | public var animationCurve: UIView.AnimationCurve = UIView.AnimationCurve.easeInOut 9 | /// Keyboard animation duration 10 | public var animationDuration: TimeInterval = 0.0 11 | 12 | /// Create new 13 | public init() {} 14 | 15 | /// Is the keyboard visible 16 | public var isVisible: Bool { 17 | height != 0 18 | } 19 | 20 | mutating func update(with userInfo: [AnyHashable: Any]?) { 21 | if let value = userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { 22 | self.height = value.height 23 | } 24 | if let value = userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber { 25 | if let curve = UIView.AnimationCurve(rawValue: value.intValue) { 26 | self.animationCurve = curve 27 | } 28 | } 29 | if let value = userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber { 30 | self.animationDuration = value.doubleValue 31 | } 32 | } 33 | } 34 | 35 | public protocol KeyboardObserver { 36 | func registerForKeyboardNotifications() 37 | func unregisterFromKeyboardNotifications() 38 | 39 | func keyboardWillShow(_ notification: Notification) 40 | func keyboardWillHide(_ notification: Notification) 41 | func keyboardWillChangeFrame(_ notification: Notification) 42 | 43 | func keyboardWillChangeToRect(_ rect: CGRect, 44 | duration: TimeInterval, 45 | curve: UIView.AnimationCurve) 46 | } 47 | 48 | public extension KeyboardObserver where Self: UIViewController { 49 | func registerForKeyboardNotifications() { 50 | let defaultCenter = NotificationCenter.default 51 | 52 | _ = defaultCenter.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: nil) { [weak self] notification in 53 | self?.keyboardWillShow(notification) 54 | } 55 | _ = defaultCenter.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: nil) { [weak self] notification in 56 | self?.keyboardWillHide(notification) 57 | } 58 | _ = defaultCenter.addObserver(forName: UIResponder.keyboardWillChangeFrameNotification, object: nil, queue: nil) { [weak self] notification in 59 | self?.keyboardWillChangeFrame(notification) 60 | } 61 | } 62 | 63 | func unregisterFromKeyboardNotifications() { 64 | let defaultCenter = NotificationCenter.default 65 | 66 | defaultCenter.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) 67 | defaultCenter.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) 68 | defaultCenter.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil) 69 | } 70 | 71 | func keyboardWillShow(_ notification: Notification) { 72 | guard let userInfo = notification.userInfo, 73 | let rectValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, 74 | let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval, 75 | let curveInfo = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber 76 | else { 77 | return 78 | } 79 | 80 | if let curve = UIView.AnimationCurve(rawValue: curveInfo.intValue) { 81 | self.keyboardWillChangeToRect(rectValue.cgRectValue, duration: duration, curve: curve) 82 | } 83 | } 84 | 85 | func keyboardWillHide(_ notification: Notification) { 86 | guard let userInfo = notification.userInfo, 87 | let rectValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, 88 | let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval, 89 | let curveInfo = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber 90 | else { 91 | return 92 | } 93 | 94 | var rect = rectValue.cgRectValue 95 | 96 | if let delegate = UIApplication.shared.delegate { 97 | if let optionalWindow = delegate.window, let window = optionalWindow { 98 | if rect.origin.y > window.bounds.height { 99 | rect.origin.y = window.bounds.height 100 | } 101 | } 102 | } 103 | if let curve = UIView.AnimationCurve(rawValue: curveInfo.intValue) { 104 | self.keyboardWillChangeToRect(rect, duration: duration, curve: curve) 105 | } 106 | } 107 | 108 | func keyboardWillChangeFrame(_ notification: Notification) { 109 | guard let userInfo = notification.userInfo, 110 | let rectValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, 111 | let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval, 112 | let curveInfo = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber 113 | else { 114 | return 115 | } 116 | 117 | if let curve = UIView.AnimationCurve(rawValue: curveInfo.intValue) { 118 | self.keyboardWillChangeToRect(rectValue.cgRectValue, duration: duration, curve: curve) 119 | } 120 | } 121 | 122 | func keyboardWillChangeToRect(_ rect: CGRect, duration: TimeInterval, curve: UIView.AnimationCurve) { } 123 | 124 | func animateWithKeyboard( 125 | notification: Notification, 126 | animations: ((_ keyboardFrame: CGRect) -> Void)? 127 | ) { 128 | // Extract the duration of the keyboard animation 129 | let durationKey = UIResponder.keyboardAnimationDurationUserInfoKey 130 | let duration = notification.userInfo![durationKey] as! Double 131 | 132 | // Extract the final frame of the keyboard 133 | let frameKey = UIResponder.keyboardFrameEndUserInfoKey 134 | let keyboardFrameValue = notification.userInfo![frameKey] as! NSValue 135 | 136 | // Extract the curve of the iOS keyboard animation 137 | let curveKey = UIResponder.keyboardAnimationCurveUserInfoKey 138 | let curveValue = notification.userInfo![curveKey] as! Int 139 | let curve = UIView.AnimationCurve(rawValue: curveValue)! 140 | 141 | // Create a property animator to manage the animation 142 | let animator = UIViewPropertyAnimator( 143 | duration: duration, 144 | curve: curve 145 | ) { 146 | // Perform the necessary animation layout updates 147 | animations?(keyboardFrameValue.cgRectValue) 148 | 149 | // Required to trigger NSLayoutConstraint changes 150 | // to animate 151 | self.view?.layoutIfNeeded() 152 | } 153 | 154 | // Start the animation 155 | animator.startAnimation() 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/MailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MailView.swift 3 | // ExtensionKit 4 | // 5 | // Created by Gary Tokman on 9/22/24. 6 | // 7 | 8 | #if os(iOS) && canImport(MessageUI) 9 | import MessageUI 10 | import SwiftUI 11 | 12 | private struct MailView: UIViewControllerRepresentable { 13 | let item: MailItem 14 | 15 | func makeCoordinator() -> Coordinator { 16 | Coordinator() 17 | } 18 | 19 | func makeUIViewController(context: Context) -> UIViewController { 20 | context.coordinator.makeViewController(with: item) 21 | } 22 | 23 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 24 | } 25 | 26 | private extension MailView { 27 | class Coordinator: NSObject, MFMailComposeViewControllerDelegate { 28 | /// A standard interface for managing, editing, and sending an email message. 29 | func makeViewController(with request: MailItem) -> UIViewController { 30 | MFMailComposeViewController().apply { 31 | $0.mailComposeDelegate = self 32 | 33 | $0.setToRecipients(request.emails) 34 | 35 | if let subject = request.subject { 36 | $0.setSubject(subject) 37 | } 38 | 39 | if let body = request.body { 40 | $0.setMessageBody(body, isHTML: request.isHTML) 41 | } 42 | 43 | if let attachment = request.attachment { 44 | $0.addAttachmentData( 45 | attachment.data, 46 | mimeType: attachment.mimeType, 47 | fileName: attachment.fileName 48 | ) 49 | } 50 | } 51 | } 52 | 53 | func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { 54 | controller.dismiss(animated: true) 55 | } 56 | } 57 | } 58 | 59 | public struct MailItem: Identifiable, Equatable { 60 | public struct Attachment: Equatable { 61 | let data: Data 62 | let mimeType: String 63 | let fileName: String 64 | 65 | public init(data: Data, mimeType: String, fileName: String) { 66 | self.data = data 67 | self.mimeType = mimeType 68 | self.fileName = fileName 69 | } 70 | } 71 | 72 | public let id = UUID() 73 | let emails: [String] 74 | let subject: String? 75 | let body: String? 76 | let isHTML: Bool 77 | let attachment: Attachment? 78 | 79 | public init( 80 | emails: String..., 81 | subject: String? = nil, 82 | body: String? = nil, 83 | isHTML: Bool = true, 84 | attachment: Attachment? = nil 85 | ) { 86 | self.emails = emails 87 | self.subject = subject 88 | self.body = body 89 | self.isHTML = isHTML 90 | self.attachment = attachment 91 | } 92 | } 93 | 94 | public extension View { 95 | /// Presents a compose mail sheet using the given options for sending a message. 96 | /// 97 | /// If the current device is not able to send email, determined via `MFMailComposeViewController.canSendMail()`, 98 | /// an alert will notify the user of the failure. 99 | @ViewBuilder 100 | func sheet(mail item: Binding) -> some View { 101 | if MFMailComposeViewController.canSendMail() { 102 | sheet(item: item) { item in 103 | MailView(item: item) 104 | .ignoresSafeArea() 105 | } 106 | } else { 107 | alert(item: item) { _ in 108 | Alert( 109 | title: Text("Could Not Send Email"), 110 | message: Text("Your device could not send email.") 111 | ) 112 | } 113 | } 114 | } 115 | } 116 | 117 | #endif 118 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/MessageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageView.swift 3 | // mobile 4 | // 5 | // Created by Gary Tokman on 5/13/24. 6 | // 7 | 8 | import MessageUI 9 | import SwiftUI 10 | import UIKit 11 | 12 | public struct MessageItem: Identifiable, Equatable { 13 | public var id = UUID() 14 | public var recipients: [String] 15 | public var body: String 16 | 17 | public init(recipients: [String], body: String) { 18 | self.recipients = recipients 19 | self.body = body 20 | } 21 | } 22 | 23 | @available(iOS 17.0, *) 24 | struct MessageView: UIViewControllerRepresentable { 25 | typealias Completion = (_ messageSent: Bool) -> Void 26 | 27 | static var canSendText: Bool { MFMessageComposeViewController.canSendText() } 28 | 29 | let item: MessageItem 30 | let completion: Completion? 31 | 32 | func makeUIViewController(context: Context) -> UIViewController { 33 | guard Self.canSendText else { 34 | let errorView = MessagesUnavailableView() 35 | return UIHostingController(rootView: errorView) 36 | } 37 | 38 | let controller = MFMessageComposeViewController() 39 | controller.messageComposeDelegate = context.coordinator 40 | controller.recipients = item.recipients 41 | controller.body = item.body 42 | 43 | return controller 44 | } 45 | 46 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 47 | 48 | func makeCoordinator() -> Coordinator { 49 | Coordinator(completion: self.completion) 50 | } 51 | 52 | class Coordinator: NSObject, MFMessageComposeViewControllerDelegate { 53 | private let completion: Completion? 54 | 55 | public init(completion: Completion?) { 56 | self.completion = completion 57 | } 58 | 59 | public func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) { 60 | controller.dismiss(animated: true, completion: nil) 61 | completion?(result == .sent) 62 | } 63 | } 64 | } 65 | 66 | @available(iOS 17.0, *) 67 | struct MessagesUnavailableView: View { 68 | var body: some View { 69 | ContentUnavailableView( 70 | "Messages Not Supported", 71 | systemImage: "message.fill", 72 | description: Text("Switch to a device that has messages.") 73 | ) 74 | } 75 | } 76 | 77 | @available(iOS 17.0, *) 78 | public extension View { 79 | /// Presents a message composer view for sending a text message. 80 | /// 81 | /// - Parameters: 82 | /// - recipients: An array of phone numbers to which the message should be sent. 83 | /// - body: The body of the message to be sent. 84 | /// - completion: A closure that is called with a boolean value indicating whether the message was sent successfully. 85 | @ViewBuilder 86 | func sheet(message item: Binding, completion: @escaping (Bool) -> Void) -> some View { 87 | if MFMessageComposeViewController.canSendText() { 88 | sheet(item: item) { item in 89 | MessageView(item: item, completion: completion) 90 | .ignoresSafeArea() 91 | } 92 | } else { 93 | MessagesUnavailableView() 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/RoundedCorner.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @available(iOS 13.0, macOS 10.15, *) 4 | struct RoundedCorner: Shape { 5 | var radius: CGFloat = .infinity 6 | var corners: UIRectCorner = .allCorners 7 | 8 | func path(in rect: CGRect) -> Path { 9 | let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) 10 | return Path(path.cgPath) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/SafariView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariView.swift 3 | // ExtensionKit 4 | // 5 | // Created by Gary Tokman on 9/22/24. 6 | // 7 | 8 | #if os(iOS) && canImport(SafariServices) 9 | import SafariServices 10 | import SwiftUI 11 | 12 | private struct SafariView: UIViewControllerRepresentable { 13 | let url: URL 14 | 15 | func makeUIViewController(context: Context) -> UIViewController { 16 | SFSafariViewController(url: url) 17 | } 18 | 19 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 20 | } 21 | 22 | public extension View { 23 | /// Presents a Safari sheet using the given URL for browsing the web. 24 | /// 25 | /// struct ContentView: View { 26 | /// @State private var linkURL: URL? 27 | /// 28 | /// var body: some View { 29 | /// VStack { 30 | /// Button(action: { linkURL = "https://apple.com" }) 31 | /// Text("Open link") 32 | /// } 33 | /// .sheet(safari: $linkURL) 34 | /// } 35 | /// } 36 | /// } 37 | /// 38 | /// - Parameters: 39 | /// - url: A binding to whether the Safari browser should be presented. 40 | func sheet(safari url: Binding) -> some View { 41 | sheet( 42 | isPresented: Binding( 43 | get: { url.wrappedValue != nil }, 44 | set: { 45 | guard !$0 else { return } 46 | url.wrappedValue = nil 47 | } 48 | ) 49 | ) { 50 | if let url = url.wrappedValue { 51 | SafariView(url: url) 52 | .ignoresSafeArea() 53 | } 54 | } 55 | } 56 | } 57 | #endif 58 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/Scroll/ScrollViewOffSetReader.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKit 3 | 4 | /// Hosting controller that updates the scroll view offset (x,y) 5 | class ScrollViewOffSetReader: UIHostingController where Content: View { 6 | 7 | var offset: Binding 8 | let isOffsetX: Bool 9 | var showed = false 10 | var sv: UIScrollView? 11 | 12 | init(offset: Binding, isOffsetX: Bool, rootView: Content) { 13 | self.offset = offset 14 | self.isOffsetX = isOffsetX 15 | super.init(rootView: rootView) 16 | } 17 | 18 | @objc dynamic required init?(coder aDecoder: NSCoder) { 19 | fatalError("init(coder:) has not been implemented") 20 | } 21 | 22 | override func viewDidAppear(_ animated: Bool) { 23 | if showed { 24 | return 25 | } 26 | showed = true 27 | 28 | sv = findScrollView(in: view) 29 | 30 | sv?.addObserver(self, 31 | forKeyPath: #keyPath(UIScrollView.contentOffset), 32 | options: [.old, .new], 33 | context: nil) 34 | 35 | scroll(to: offset.wrappedValue, animated: false 36 | ) 37 | 38 | super.viewDidAppear(animated) 39 | } 40 | 41 | func scroll(to offset: CGPoint, animated: Bool = true) { 42 | if let s = sv { 43 | if offset != self.offset.wrappedValue { 44 | let offset = self.isOffsetX 45 | ? CGPoint(x: offset.x, y: 0) 46 | : CGPoint(x: 0, y: offset.y) 47 | s.setContentOffset(offset, animated: animated) 48 | } 49 | } 50 | } 51 | 52 | override func observeValue(forKeyPath keyPath: String?, 53 | of object: Any?, 54 | change: [NSKeyValueChangeKey: Any]?, 55 | context: UnsafeMutableRawPointer?) { 56 | if keyPath == #keyPath(UIScrollView.contentOffset) { 57 | if let s = self.sv { 58 | DispatchQueue.main.async { 59 | self.offset.wrappedValue = s.contentOffset 60 | } 61 | } 62 | } 63 | } 64 | 65 | func findScrollView(in view: UIView?) -> UIScrollView? { 66 | if view?.isKind(of: UIScrollView.self) ?? false { 67 | return view as? UIScrollView 68 | } 69 | 70 | for subview in view?.subviews ?? [] { 71 | if let sv = findScrollView(in: subview) { 72 | return sv 73 | } 74 | } 75 | 76 | return nil 77 | } 78 | } 79 | 80 | struct ScrollViewOffSetReaderRepresentable: UIViewControllerRepresentable where Content: View { 81 | 82 | var offset: Binding 83 | let isOffsetX: Bool 84 | let content: Content 85 | 86 | init(offset: Binding, isOffsetX: Bool, @ViewBuilder content: @escaping () -> Content) { 87 | 88 | self.offset = offset 89 | self.isOffsetX = isOffsetX 90 | self.content = content() 91 | } 92 | 93 | func makeUIViewController(context: Context) -> ScrollViewOffSetReader { 94 | ScrollViewOffSetReader(offset: offset, isOffsetX: isOffsetX, rootView: content) 95 | } 96 | 97 | func updateUIViewController(_ uiViewController: ScrollViewOffSetReader, context: Context) { 98 | uiViewController.scroll(to: offset.wrappedValue, animated: true) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/Shape.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @available(iOS 13.0, OSX 10.15, *) 4 | public extension Shape { 5 | 6 | /// Addd a `LinearGradient` fill on Shape 7 | /// - Parameters: 8 | /// - colors: Colors 9 | /// - start: Start, default top 10 | /// - end: End, default bottom 11 | /// - Returns: View 12 | func gradientFill( 13 | colors: Color..., 14 | start: UnitPoint = .top, 15 | end: UnitPoint = .bottom) -> some View { 16 | fill( 17 | LinearGradient( 18 | gradient: Gradient(colors: colors), 19 | startPoint: start, endPoint: end) 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/StoreView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariView.swift 3 | // ExtensionKit 4 | // 5 | // Created by Gary Tokman on 9/22/24. 6 | // 7 | 8 | #if os(iOS) && canImport(StoreKit) 9 | import StoreKit 10 | import SwiftUI 11 | 12 | public extension View { 13 | /// Presents iTunes Store product information using the given App Store parameters. 14 | /// 15 | /// - Parameters: 16 | /// - itunesID: The iTunes identifier for the item you want the store to display when the view controller is presented. 17 | /// - affiliateToken: The affiliate identifier you wish to use for any purchase made through the view controller. 18 | /// - campaignToken: An App Analytics campaign. 19 | /// - application: The application to present the store page from. 20 | /// - completion: The block to execute after the presentation finishes. 21 | func open( 22 | itunesID: String, 23 | affiliateToken: String? = nil, 24 | campaignToken: String? = nil, 25 | application: UIApplication, 26 | completion: (() -> Void)? = nil 27 | ) { 28 | // Temporary solution until supported by SwiftUI: 29 | // https://stackoverflow.com/q/64503955 30 | let viewController = SKStoreProductViewController() 31 | var parameters = [SKStoreProductParameterITunesItemIdentifier: itunesID] 32 | 33 | if let affiliateToken = affiliateToken { 34 | parameters[SKStoreProductParameterAffiliateToken] = affiliateToken 35 | } 36 | 37 | if let campaignToken = campaignToken { 38 | parameters[SKStoreProductParameterCampaignToken] = campaignToken 39 | } 40 | 41 | viewController.loadProduct(withParameters: parameters) { loaded, _ in 42 | guard let rootViewController = application.connectedScenes 43 | .filter({ $0.activationState == .foregroundActive }) 44 | .compactMap({ $0 as? UIWindowScene }) 45 | .first? 46 | .windows 47 | .filter(\.isKeyWindow) 48 | .first? 49 | .rootViewController, 50 | loaded 51 | else { 52 | completion?() 53 | return 54 | } 55 | 56 | rootViewController.present(viewController, animated: true, completion: completion) 57 | } 58 | } 59 | } 60 | #endif 61 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/SwiftUI/Text.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension Text { 4 | 5 | /// Dateformatter 6 | static let dateFormatter = DateFormatter() 7 | 8 | /// Create Text from date formate 9 | /// - Parameters: 10 | /// - date: `Date` 11 | /// - dateStyle: `DateFormatter.Style`, default .full 12 | init(_ date: Date, dateStyle: DateFormatter.Style = .full) { 13 | let formatter = Text.dateFormatter 14 | formatter.dateStyle = dateStyle 15 | self.init(verbatim: formatter.string(from: date)) 16 | } 17 | 18 | /// Create Text with date formatter string 19 | /// - Parameters: 20 | /// - date: `Date` 21 | /// - formatter: `DateFormatter` 22 | init(_ date: Date, formatter: DateFormatter = Text.dateFormatter) { 23 | self.init(verbatim: formatter.string(from: date)) 24 | } 25 | 26 | /// Text with system font 27 | /// - Parameters: 28 | /// - size: size, default 18 29 | /// - weight: font weight, default regular 30 | /// - design: font design, default .default 31 | /// - Returns: View 32 | func system(_ size: CGFloat = 18, weight: SwiftUI.Font.Weight = .regular, design: SwiftUI.Font.Design = .default) -> Text { 33 | self.font(.system(size: size, weight: weight, design: design)) 34 | } 35 | 36 | /// Text with system font and monospaced 37 | /// - Parameters: 38 | /// - size: size, default 18 39 | /// - weight: font weight, default regular 40 | /// - design: font design, default .monospaced 41 | /// - Returns: View 42 | func monospaced(_ size: CGFloat = 18, weight: SwiftUI.Font.Weight = .regular, design: SwiftUI.Font.Design = .monospaced) -> Text { 43 | self.font(.system(size: size, weight: weight, design: design)) 44 | } 45 | 46 | /// Hidden `View` with frame of String vale 47 | /// - Parameter value: String 48 | /// - Returns: View 49 | func templateSize(for value: String) -> some View { 50 | return Text(value).hidden().overlay(self) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Transition/SharedTransitioning.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// This generic protocol is used to enable shared element transitions between view controllers. 4 | /// It defines the following requirements: 5 | /// 6 | /// `sharedFrame`: Gets the frame of the shared element in window coordinates 7 | /// `sharedView`: Gets the view of the shared element in window coordinates. 8 | /// `config`: Optional configuration for the transition 9 | /// `prepare`: A method to prepare the view controller just before transitioning 10 | /// 11 | public protocol SharedTransitioning { 12 | var sharedFrame: CGRect { get } 13 | var sharedView: UIView? { get } 14 | var config: SharedTransitionConfig? { get } 15 | func prepare(for transition: TransitionType) 16 | } 17 | 18 | /// Default methods and properties of the `SharedTransitioning` protocol. 19 | /// They're optional so view controllers don't need to implement them if they don't have to. 20 | /// This means conforming types only need to implement `sharedFrame`. 21 | /// 22 | public extension SharedTransitioning { 23 | var sharedView: UIView? { nil } 24 | var config: SharedTransitionConfig? { nil } 25 | func prepare(for transition: TransitionType) {} 26 | } 27 | 28 | public extension UIViewControllerContextTransitioning { 29 | /// A helper method that takes a transition context key (from/to view controller) and 30 | /// grabs the shared **frame** from the view controller(s) that will be used during the custom transition. 31 | /// 32 | /// We need this to calculate the starting and ending positions of a view during a transition animation. 33 | /// For example, when transitioning from a grid of photos to a detail view, it determines where the image 34 | /// is located in both screens. 35 | /// 36 | /// It returns the `sharedFrame` if the view controller conforms to `SharedTransitioning`. 37 | /// 38 | func sharedFrame(forKey key: UITransitionContextViewControllerKey) -> CGRect? { 39 | let viewController = viewController(forKey: key) 40 | 41 | // We need layoutIfNeeded() here because: 42 | // 1. The view hierarchy might not be fully laid out yet 43 | // 2. Any pending Auto Layout changes need to be applied 44 | // 3. Without it, frame calculations might be incorrect or zero 45 | viewController?.view.layoutIfNeeded() 46 | 47 | return (viewController as? SharedTransitioning)?.sharedFrame 48 | } 49 | 50 | /// A helper method that takes a transition context key (from/to view controller) and 51 | /// grabs the shared **view** from the view controller(s) that will be used during the custom transition. 52 | /// 53 | /// Essentially, this will be a fake "duplicated" view that appears to seamlessly travel across from the source 54 | /// view to the destination view, and only exists during the transition. 55 | /// 56 | /// It returns the `sharedView` if the view controller conforms to `SharedTransitioning` and 57 | /// requires it for the transition. 58 | /// 59 | func sharedView(forKey key: UITransitionContextViewControllerKey) -> UIView? { 60 | let viewController = viewController(forKey: key) 61 | viewController?.view.layoutIfNeeded() 62 | 63 | let sharedView = (viewController as? SharedTransitioning)?.sharedView 64 | return sharedView 65 | } 66 | } 67 | 68 | /// This is a configuration struct that controls various aspects of the shared element transition animation. 69 | /// 70 | /// `duration`: How long the transition animation takes 71 | /// `curve`: The timing function that controls the animation's acceleration/deceleration 72 | /// `maskCornerRadius`: Controls the corner radius of the transitioning view 73 | /// `overlayOpacity`: The opacity of the backdrop during transition 74 | /// `interactionScaleFactor`: Controls max scale factor depending on interactive or default transition 75 | /// `placeholderColor`: The color of the final frame of the transitioning view. 76 | /// 77 | public struct SharedTransitionConfig { 78 | public var duration: CGFloat 79 | public var curve: CAMediaTimingFunction 80 | public var maskCornerRadius: CGFloat 81 | public var overlayOpacity: Float 82 | public var interactionScaleFactor: CGFloat = .zero 83 | public var placeholderColor: UIColor 84 | } 85 | 86 | public extension SharedTransitionConfig { 87 | // Default configuration for non-interactive transitions 88 | static var `default`: SharedTransitionConfig { 89 | .init( 90 | duration: 0.25, 91 | curve: CAMediaTimingFunction(controlPoints: 0.25, 0.46, 0.45, 0.94), 92 | maskCornerRadius: UIScreen.main.displayCornerRadius, 93 | overlayOpacity: 0.5, 94 | placeholderColor: .white 95 | ) 96 | } 97 | 98 | // Configuration for interactive transitions (e.g., pan gesture) 99 | static var interactive: SharedTransitionConfig { 100 | .init( 101 | duration: 0.25, 102 | curve: CAMediaTimingFunction(controlPoints: 0.25, 0.46, 0.45, 0.94), 103 | maskCornerRadius: UIScreen.main.displayCornerRadius, 104 | overlayOpacity: 0.5, 105 | interactionScaleFactor: 0.6, 106 | placeholderColor: .white 107 | ) 108 | } 109 | 110 | static var interpolation: SharedTransitionConfig { 111 | .init( 112 | duration: 0.35, 113 | curve: CAMediaTimingFunction(controlPoints: 0.25, 0.46, 0.45, 0.94), 114 | maskCornerRadius: 0, 115 | overlayOpacity: 0, 116 | placeholderColor: .systemBackground 117 | ) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Transition/TransitionType.swift: -------------------------------------------------------------------------------- 1 | /// Convenience enum for determining the transition types for our custom transitions 2 | /// 3 | public enum TransitionType { 4 | case push 5 | case pop 6 | } 7 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/UIKit/CALayer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public extension CALayer { 5 | func makeSnapshot() -> UIImage? { 6 | let scale = UIScreen.main.scale 7 | UIGraphicsBeginImageContextWithOptions(frame.size, false, scale) 8 | defer { UIGraphicsEndImageContext() } 9 | guard let context = UIGraphicsGetCurrentContext() else { return nil } 10 | render(in: context) 11 | let screenshot = UIGraphicsGetImageFromCurrentImageContext() 12 | return screenshot 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/UIKit/UIButton.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import UIKit 4 | 5 | #endif 6 | 7 | public extension UIButton { 8 | 9 | @IBInspectable var normalStateBackgroundColor: UIColor? { 10 | // not gettable 11 | get { return nil } 12 | set { 13 | if let color = newValue { 14 | setBackgroundColor(color, for: .normal) 15 | } 16 | } 17 | } 18 | @IBInspectable var disabledStateBackgroundColor: UIColor? { 19 | // not gettable 20 | get { return nil } 21 | set { 22 | if let color = newValue { 23 | setBackgroundColor(color, for: .disabled) 24 | } 25 | } 26 | } 27 | @IBInspectable var highlightedStateBackgroundColor: UIColor? { 28 | // not gettable 29 | get { return nil } 30 | set { 31 | if let color = newValue { 32 | setBackgroundColor(color, for: .highlighted) 33 | } 34 | } 35 | } 36 | @IBInspectable var selectedStateBackgroundColor: UIColor? { 37 | // not gettable 38 | get { return nil } 39 | set { 40 | if let color = newValue { 41 | setBackgroundColor(color, for: .selected) 42 | } 43 | } 44 | } 45 | 46 | /// Set background color for state 47 | /// - Parameters: 48 | /// - color: color 49 | /// - state: state 50 | func setBackgroundColor(_ color: UIColor, for state: UIControl.State) { 51 | let rect = CGRect(x: 0, y: 0, width: 1, height: 1) 52 | if let image = UIImage.image(withPureColor: color, for: rect, rounded: false) { 53 | setBackgroundImage(image, for: state) 54 | } else { 55 | dprint("UIButton.setBackgroundColor(_:for:) got called but returning a nil image!") 56 | } 57 | } 58 | 59 | var isTitleImagePositionReversed: Bool { 60 | get { 61 | return transform == .identity 62 | } 63 | set { 64 | let reversingTransform = CGAffineTransform(scaleX: -1.0, y: 1.0) 65 | transform = reversingTransform 66 | titleLabel?.transform = reversingTransform 67 | imageView?.transform = reversingTransform 68 | } 69 | } 70 | 71 | var backgroundImageView: UIImageView? { 72 | return subviews.first { 73 | if let backgroundImageView = $0 as? UIImageView, backgroundImageView != imageView { 74 | return true 75 | } 76 | return false 77 | } as? UIImageView 78 | } 79 | 80 | 81 | private class ButtonClosureWrapper { 82 | 83 | let closure: () -> Void 84 | 85 | init (_ closure: @escaping () -> Void) { 86 | self.closure = closure 87 | } 88 | 89 | @objc func invoke () { 90 | closure() 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/UIKit/UICollectionReusableView+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UICollectionReusableView { 4 | /// Gives each cell an identifier that is derived from its `String(describing: self)` 5 | /// 6 | static var identifier: String { 7 | return String(describing: self) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/UIKit/UICollectionView+Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UICollectionView { 4 | /// Enum representing a cell's position based on whether it's 5 | /// beyond the top or bottom of the view, or entirely visible 6 | /// 7 | enum VerticalCellPosition { 8 | case overTop 9 | case visible 10 | case overBottom 11 | } 12 | 13 | /// The visible area of the `UICollectionView` 14 | /// 15 | var visibleRect: CGRect { 16 | CGRect(origin: contentOffset, size: bounds.size) 17 | } 18 | 19 | /// The maximum offset of the `UICollectionView`, i.e. the bottom and trailing edge 20 | /// 21 | var maxOffset: CGPoint { 22 | CGPoint( 23 | x: contentSize.width - bounds.size.width + safeAreaInsets.right + contentInset.right, 24 | y: contentSize.height - bounds.size.height + safeAreaInsets.bottom + contentInset.bottom 25 | ) 26 | } 27 | 28 | /// The minimum offset of the `UICollectionView`, i.e. the top and leading edge 29 | /// 30 | var minOffset: CGPoint { 31 | CGPoint( 32 | x: -safeAreaInsets.left - contentInset.left, 33 | y: -safeAreaInsets.top - contentInset.top 34 | ) 35 | } 36 | 37 | /// Retrieves the `VerticalCellPosition` of a cell in a `UICollectionView` based on its position 38 | /// 39 | private func verticalCellPosition(for indexPath: IndexPath) -> VerticalCellPosition? { 40 | guard let attributes = layoutAttributesForItem(at: indexPath) else { return nil } 41 | if attributes.frame.minY < visibleRect.minY { 42 | return .overTop 43 | } else if attributes.frame.maxY > visibleRect.maxY { 44 | return .overBottom 45 | } else { 46 | return .visible 47 | } 48 | } 49 | 50 | /// Scrolls to a specified item in a `UICollectionView` such that it becomes entirely visible, 51 | /// along with some additional padding specified per collection view 52 | /// 53 | func verticalScrollItemVisible(at indexPath: IndexPath, 54 | withPadding padding: CGFloat, 55 | animated: Bool) 56 | { 57 | switch verticalCellPosition(for: indexPath) { 58 | case .overTop: 59 | verticalScrollToItem(at: indexPath, forCellPosition: .overTop, padding: padding, animated: animated) 60 | 61 | case .overBottom: 62 | verticalScrollToItem(at: indexPath, forCellPosition: .overBottom, padding: padding, animated: animated) 63 | 64 | default: 65 | return 66 | } 67 | } 68 | 69 | /// Helper function used in `verticalScrollItemVisible` that scrolls to a specified item 70 | /// in a `UICollectionView` depending on its `cellPosition` 71 | /// 72 | func verticalScrollToItem(at indexPath: IndexPath, 73 | forCellPosition cellPosition: VerticalCellPosition, 74 | padding: CGFloat, 75 | animated: Bool) 76 | { 77 | guard let attributes = layoutAttributesForItem(at: indexPath) else { return } 78 | 79 | switch cellPosition { 80 | case .overTop: 81 | var offset = attributes.frame.origin.y - padding 82 | offset = min(max(offset, minOffset.y), maxOffset.y) 83 | setContentOffset(.init(x: 0, y: offset), animated: animated) 84 | 85 | case .overBottom: 86 | var offset = attributes.frame.origin.y - bounds.height + attributes.frame.height + padding 87 | offset = min(max(offset, minOffset.y), maxOffset.y) 88 | setContentOffset(.init(x: 0, y: offset), animated: animated) 89 | 90 | default: 91 | break 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/UIKit/UIColor.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import UIKit 4 | 5 | #endif 6 | 7 | public extension UIColor { 8 | 9 | /// New color from RGB value 10 | /// - Parameters: 11 | /// - rgbValue: value 12 | /// - alpha: alpha 13 | convenience init(rgbValue: UInt, alpha: CGFloat = 1) { 14 | self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, 15 | green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, 16 | blue: CGFloat(rgbValue & 0x0000FF) / 255.0, 17 | alpha: alpha) 18 | } 19 | 20 | /// Color from HEX 21 | /// - Parameter hexCode: Hex w/o `#` 22 | convenience init(hexCode: String, alpha: CGFloat = 1) { 23 | var cString:String = hexCode.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() 24 | if (cString.hasPrefix("#")) { 25 | cString.remove(at: cString.startIndex) 26 | } 27 | if ((cString.count) != 6) { 28 | self.init() 29 | } else { 30 | var rgbValue:UInt64 = 0 31 | Scanner(string: cString).scanHexInt64(&rgbValue) 32 | self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, 33 | green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, 34 | blue: CGFloat(rgbValue & 0x0000FF) / 255.0, 35 | alpha: alpha) 36 | } 37 | } 38 | 39 | /// Check whether self is a light/bright color. 40 | /// https://stackoverflow.com/questions/2509443/check-if-uicolor-is-dark-or-bright 41 | var isLight: Bool { 42 | guard let components = cgColor.components, components.count > 2 else { return false } 43 | let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 44 | return brightness > 0.5 45 | } 46 | 47 | /// Check whether self is a light/bright color. 48 | /// https://stackoverflow.com/questions/2509443/check-if-uicolor-is-dark-or-bright 49 | var isExtremelyLight: Bool { 50 | guard let components = cgColor.components, components.count > 2 else { return false } 51 | let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 52 | return brightness > 0.9 53 | } 54 | 55 | /// Random color 56 | /// - Parameter alpha: alpha 57 | /// - Returns: new color 58 | static func random(alpha: CGFloat = 1.0) -> UIColor { 59 | let r = CGFloat.random(in: 0...1) 60 | let g = CGFloat.random(in: 0...1) 61 | let b = CGFloat.random(in: 0...1) 62 | 63 | return UIColor(red: r, green: g, blue: b, alpha: alpha) 64 | } 65 | 66 | /// Darken color 67 | /// - Parameter percentage: percentage 68 | /// - Returns: new color 69 | func lighten(by percentage: CGFloat = 30.0) -> UIColor { 70 | return self.adjust(by: abs(percentage) ) 71 | } 72 | 73 | /// Darken color 74 | /// - Parameter percentage: percentage 75 | /// - Returns: new color 76 | func darken(by percentage: CGFloat = 30.0) -> UIColor { 77 | return self.adjust(by: -1 * abs(percentage) ) 78 | } 79 | 80 | /// Color for UI appearance ex: dark/light mode 81 | /// - Parameters: 82 | /// - light: Light Color 83 | /// - dark: Dark Color 84 | /// - Returns: UIColor 85 | static func dynamicColor(light: UIColor, dark: UIColor) -> UIColor { 86 | UIColor { traitCollection -> UIColor in 87 | if traitCollection.userInterfaceStyle == .dark { 88 | return dark 89 | } else { 90 | return light 91 | } 92 | } 93 | } 94 | 95 | private func adjust(by percentage: CGFloat = 30.0) -> UIColor { 96 | var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 97 | if self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) { 98 | return UIColor(red: min(red + percentage/100, 1.0), 99 | green: min(green + percentage/100, 1.0), 100 | blue: min(blue + percentage/100, 1.0), 101 | alpha: alpha) 102 | } else { 103 | return self 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/UIKit/UIDevice.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public extension UIDevice { 4 | var hasNotch: Bool { 5 | if #available(iOS 13.0, *) { 6 | let scenes = UIApplication.shared.connectedScenes 7 | let windowScene = scenes.first as? UIWindowScene 8 | guard let window = windowScene?.windows.first else { return false } 9 | 10 | return window.safeAreaInsets.top > 20 11 | } 12 | 13 | if #available(iOS 11.0, *) { 14 | let top = UIApplication.keyWindow?.safeAreaInsets.top ?? .zero 15 | return top > 20 16 | } else { 17 | // Fallback on earlier versions 18 | return false 19 | } 20 | } 21 | 22 | var safeAreaInsets: UIEdgeInsets { 23 | return UIApplication.keyWindow?.safeAreaInsets ?? .zero 24 | } 25 | 26 | var topInset: CGFloat { 27 | return safeAreaInsets.top 28 | } 29 | 30 | var bottomInset: CGFloat { 31 | return safeAreaInsets.bottom 32 | } 33 | 34 | var displaySize: CGSize { 35 | return UIApplication.keyWindow?.bounds.size ?? .zero 36 | } 37 | 38 | var displayCornerRadius: CGFloat { 39 | UIScreen.main.displayCornerRadius 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/UIKit/UIScreen.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// Wrapper to access main screen size 4 | public struct Screen { 5 | 6 | /// Main screen width 7 | public static let width = UIScreen.main.bounds.width 8 | 9 | /// Main screen height 10 | public static let height = UIScreen.main.bounds.height 11 | 12 | /// Main screen bounds 13 | public static let bounds = UIScreen.main.bounds 14 | 15 | /// Main screen origin 16 | public static let origin = UIScreen.main.bounds.origin 17 | 18 | } 19 | 20 | public extension UIScreen { 21 | private static let cornerRadiusKey: String = { 22 | let base64Components = [ 23 | "UmFkaXVz", // "Radius" 24 | "Q29ybmVy", // "Corner" 25 | "ZGlzcGxheQ==", // "display" 26 | "Xw==" // "_" 27 | ] 28 | 29 | return base64Components 30 | .map { Data(base64Encoded: $0)! } 31 | .compactMap { String(data: $0, encoding: .utf8) } 32 | .reversed() 33 | .joined() 34 | }() 35 | 36 | var displayCornerRadius: CGFloat { 37 | let key = Data(Self.cornerRadiusKey.utf8) 38 | .base64EncodedString() 39 | .data(using: .utf8) 40 | .flatMap { Data(base64Encoded: $0) } 41 | .flatMap { String(data: $0, encoding: .utf8) } ?? Self.cornerRadiusKey 42 | 43 | guard let cornerRadius = self.value(forKey: key) as? CGFloat else { 44 | assertionFailure("Failed to detect screen corner radius") 45 | return 0 46 | } 47 | 48 | return cornerRadius 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/UIKit/UITableView.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import UIKit 4 | 5 | #endif 6 | 7 | public extension UITableView { 8 | 9 | /// Zero rows in all sections 10 | var isEmpty: Bool { 11 | return numberOfSections.range.reduce(0, { 12 | $0 + numberOfRows(inSection: $1) 13 | }) == 0 14 | } 15 | 16 | /// Reload cell at index path 17 | /// - Parameters: 18 | /// - cell: cell 19 | /// - animation: animation 20 | func reloadCell(_ cell: UITableViewCell, with animation: UITableView.RowAnimation) { 21 | if let indexPath = indexPath(for: cell) { 22 | reloadRows(at: [indexPath], with: animation) 23 | } 24 | } 25 | 26 | /// Scroll to last index in last section 27 | /// - Parameters: 28 | /// - position: position 29 | /// - animated: animated 30 | func scrollToBottom(position: UITableView.ScrollPosition = .bottom, animated: Bool = true) { 31 | if let lastSection = (0 ..< numberOfSections).reversed().first(where: { numberOfRows(inSection: $0) > 0 }) { 32 | let lastRow = numberOfRows(inSection: lastSection) - 1 33 | let lastIndexPath = IndexPath(row: lastRow, section: lastSection) 34 | scrollToRow(at: lastIndexPath, at: position, animated: animated) 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/UIKit/ViewControllerIdentifiable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public protocol ViewControllerIdentifiable { 4 | var stringIdentifier: String { get } 5 | var nameIdentifier: String { get } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Utils/Constants.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public final class GlobalConstants { 4 | public static let screenW = UIScreen.main.bounds.width 5 | public static let screenH = UIScreen.main.bounds.height 6 | public static let colors: [UIColor] = [.red, .orange, .yellow, .green, .blue, .systemPink, .purple, .systemIndigo] 7 | } 8 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Utils/PHAsset+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Photos 2 | 3 | public extension PHAsset { 4 | /// Retrieve the filename of a photo from the photos library using information from `PHAsset` 5 | /// 6 | var originalFilename: String? { 7 | var fileName: String? 8 | 9 | if #available(iOS 9.0, *) { 10 | let resources = PHAssetResource.assetResources(for: self) 11 | if let resource = resources.first { 12 | fileName = resource.originalFilename 13 | } 14 | } 15 | 16 | if fileName == nil { 17 | /// This is an undocumented workaround that works as of iOS 9.1 18 | fileName = self.value(forKey: "filename") as? String 19 | } 20 | 21 | return fileName 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Utils/RandomColor.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public func getRandomColor() -> UIColor { 4 | let hue = CGFloat(arc4random() % 256) / 256.0 5 | let saturation: CGFloat = 0.3 + CGFloat(arc4random() % 128) / 256.0 6 | let brightness: CGFloat = 0.7 + CGFloat(arc4random() % 128) / 256.0 7 | return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0) 8 | } 9 | -------------------------------------------------------------------------------- /Sources/ExtensionKit/Utils/Then.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable all 2 | // The MIT License (MIT) 3 | // 4 | // Copyright (c) 2015 Suyeol Jeon (xoul.kr) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | #if !os(Linux) 26 | import CoreGraphics 27 | #endif 28 | #if os(iOS) || os(tvOS) 29 | import UIKit.UIGeometry 30 | #endif 31 | 32 | public protocol Then {} 33 | 34 | extension Then where Self: Any { 35 | 36 | /// Makes it available to set properties with closures just after initializing and copying the value types. 37 | /// 38 | /// let frame = CGRect().with { 39 | /// $0.origin.x = 10 40 | /// $0.size.width = 100 41 | /// } 42 | @inlinable 43 | public func with(_ block: (inout Self) throws -> Void) rethrows -> Self { 44 | var copy = self 45 | try block(©) 46 | return copy 47 | } 48 | 49 | /// Makes it available to execute something with closures. 50 | /// 51 | /// UserDefaults.standard.do { 52 | /// $0.set("devxoul", forKey: "username") 53 | /// $0.set("devxoul@gmail.com", forKey: "email") 54 | /// $0.synchronize() 55 | /// } 56 | @inlinable 57 | public func `do`(_ block: (Self) throws -> Void) rethrows { 58 | try block(self) 59 | } 60 | 61 | } 62 | 63 | extension Then where Self: AnyObject { 64 | 65 | /// Makes it available to set properties with closures just after initializing. 66 | /// 67 | /// let label = UILabel().then { 68 | /// $0.textAlignment = .center 69 | /// $0.textColor = UIColor.black 70 | /// $0.text = "Hello, World!" 71 | /// } 72 | @inlinable 73 | public func then(_ block: (Self) throws -> Void) rethrows -> Self { 74 | try block(self) 75 | return self 76 | } 77 | 78 | } 79 | 80 | extension NSObject: Then {} 81 | 82 | #if !os(Linux) 83 | extension CGPoint: Then {} 84 | extension CGRect: Then {} 85 | extension CGSize: Then {} 86 | extension CGVector: Then {} 87 | #endif 88 | 89 | extension Array: Then {} 90 | extension Dictionary: Then {} 91 | extension Set: Then {} 92 | 93 | #if os(iOS) || os(tvOS) 94 | extension UIEdgeInsets: Then {} 95 | extension UIOffset: Then {} 96 | extension UIRectEdge: Then {} 97 | #endif 98 | // swiftlint:enable all 99 | -------------------------------------------------------------------------------- /Tests/ExtensionKitTests/ExtensionKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ExtensionKit 3 | 4 | final class ExtensionKitTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(ExtensionKit().text, "Hello, World!") 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ExtensionKitTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(ExtensionKitTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import ExtensionKitTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += ExtensionKitTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:ios) 17 | 18 | platform :ios do 19 | desc "Publish changelog & make a new release on GH" 20 | lane :release do 21 | tag = `git describe --abbrev=0`.chomp 22 | puts tag 23 | file = File.read("../CHANGELOG.md") 24 | lines = [] 25 | file.each_line do |line| 26 | if line.include?("## [") 27 | break unless !lines.include?("*******") 28 | lines.append("*******") 29 | lines.append(line.capitalize) 30 | else 31 | lines.append(line.capitalize) 32 | end 33 | end 34 | log = lines.drop(5).join 35 | url = `git config --get remote.origin.url`.chomp 36 | repo = url.split(":")[1].split(".")[0] 37 | 38 | github_release = set_github_release( 39 | repository_name: "gtokman/ExtensionKit", 40 | api_token: ENV["GITHUB_TOKEN"], 41 | name: tag, 42 | tag_name: tag, 43 | description: (log rescue "Checkout the [changelog](./CHANGELOG.md)."), 44 | commitish: "main", 45 | ) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew install fastlane` 16 | 17 | # Available Actions 18 | ## iOS 19 | ### ios release 20 | ``` 21 | fastlane ios release 22 | ``` 23 | Publish changelog & make a new release on GH 24 | 25 | ---- 26 | 27 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 28 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 29 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 30 | -------------------------------------------------------------------------------- /lgtm.sh: -------------------------------------------------------------------------------- 1 | swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios18.0-simulator" 2 | sourcedocs generate --all-modules 3 | git add . && git commit -m "chore: update docs" 4 | npm run release 5 | git push origin main --follow-tags 6 | bundle exec fastlane release 7 | -------------------------------------------------------------------------------- /lint-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | parserPreset: { 4 | parserOpts: { 5 | issuePrefixes: ["[A-Z]{2,}-"], 6 | }, 7 | }, 8 | rules: { 9 | "references-empty": [0, "never"], 10 | "header-max-length": [0, "always", 90], 11 | "subject-empty": [0, "never"], 12 | "type-empty": [2, "never"], 13 | "footer-leading-blank": [0, "never"], 14 | "scope-case": [0, "never"], 15 | }, 16 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extensionkit", 3 | "version": "1.21.7", 4 | "description": "A collection of UIKit and SwiftUI extensions to speed up app development", 5 | "main": "package.swift", 6 | "scripts": { 7 | "release": "standard-version" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/gtokman/ExtensionKit.git" 12 | }, 13 | "author": "Gary Tokman", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/gtokman/ExtensionKit/issues" 17 | }, 18 | "homepage": "https://github.com/gtokman/ExtensionKit#readme" 19 | } 20 | --------------------------------------------------------------------------------