├── Demo.mov ├── Logo.png ├── Example ├── Example │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── ExampleApp.swift │ ├── Example.entitlements │ └── ContentView.swift └── Example.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ └── project.pbxproj ├── .gitignore ├── .spi.yml ├── Package.swift ├── Package@swift-5.9.swift ├── Sources └── Ignition │ ├── ViewEffectModifier.swift │ ├── ViewEffect.swift │ ├── ViewEffectAnimation.swift │ ├── OpacityEffect.swift │ ├── BlurEffect.swift │ ├── ConcatenatedEffect.swift │ ├── OffsetEffect.swift │ ├── RotationEffect.swift │ ├── ScheduledViewEffectModifier.swift │ ├── ScaleEffect.swift │ ├── OverlayEffect.swift │ ├── BackgroundEffect.swift │ ├── ChangeEffectButtonStyle.swift │ └── OnChangeViewEffectModifier.swift ├── LICENSE.md ├── .github └── workflows │ └── swift.yml └── README.md /Demo.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathantannar4/Ignition/HEAD/Demo.mov -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathantannar4/Ignition/HEAD/Logo.png -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | Package.resolved -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/Example/ExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleApp.swift 3 | // Example 4 | // 5 | // Created by Nathan Tannar on 2023-09-12. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct ExampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example/Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [Ignition] 5 | platform: macos 6 | - documentation_targets: [Ignition] 7 | platform: ios 8 | - documentation_targets: [Ignition] 9 | platform: tvos 10 | - documentation_targets: [Ignition] 11 | platform: watchos 12 | - documentation_targets: [Ignition] 13 | platform: visionos 14 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "engine", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/nathantannar4/Engine", 7 | "state" : { 8 | "revision" : "fdf90836c00b32b276e487f86577ad7321c968f3", 9 | "version" : "2.1.14" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-syntax", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/swiftlang/swift-syntax", 16 | "state" : { 17 | "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", 18 | "version" : "509.0.2" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Ignition", 7 | platforms: [ 8 | .iOS(.v13), 9 | .macOS(.v10_15), 10 | .macCatalyst(.v13), 11 | .tvOS(.v13), 12 | .watchOS(.v6), 13 | .visionOS(.v1), 14 | ], 15 | products: [ 16 | .library( 17 | name: "Ignition", 18 | targets: ["Ignition"] 19 | ), 20 | ], 21 | dependencies: [ 22 | .package(url: "https://github.com/nathantannar4/Engine", from: "2.2.1"), 23 | ], 24 | targets: [ 25 | .target( 26 | name: "Ignition", 27 | dependencies: [ 28 | "Engine" 29 | ] 30 | ) 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /Package@swift-5.9.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Ignition", 7 | platforms: [ 8 | .iOS(.v13), 9 | .macOS(.v10_15), 10 | .macCatalyst(.v13), 11 | .tvOS(.v13), 12 | .watchOS(.v6), 13 | .visionOS(.v1), 14 | ], 15 | products: [ 16 | .library( 17 | name: "Ignition", 18 | targets: ["Ignition"] 19 | ), 20 | ], 21 | dependencies: [ 22 | .package(url: "https://github.com/nathantannar4/Engine", from: "2.1.14"), 23 | ], 24 | targets: [ 25 | .target( 26 | name: "Ignition", 27 | dependencies: [ 28 | "Engine" 29 | ] 30 | ) 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /Sources/Ignition/ViewEffectModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | import Engine 7 | 8 | /// A modifier that applies a `ViewEffect` 9 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 10 | @frozen 11 | public struct ViewEffectModifier< 12 | Effect: ViewEffect 13 | >: ViewModifier { 14 | 15 | public var effect: Effect 16 | public var configuration: Effect.Configuration 17 | 18 | @inlinable 19 | public init( 20 | effect: Effect, 21 | configuration: Effect.Configuration 22 | ) { 23 | self.effect = effect 24 | self.configuration = configuration 25 | } 26 | 27 | public func body(content: Content) -> some View { 28 | effect 29 | .makeBody(configuration: configuration) 30 | .viewAlias(ViewEffectConfiguration.Content.self) { 31 | content 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Ignition/ViewEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | import Engine 7 | 8 | /// The style for ``ViewEffectModifier`` 9 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 10 | public protocol ViewEffect: DynamicProperty { 11 | 12 | associatedtype Body: View 13 | @MainActor @ViewBuilder func makeBody(configuration: Configuration) -> Body 14 | 15 | typealias Configuration = ViewEffectConfiguration 16 | } 17 | 18 | /// The configuration parameters for ``ViewEffect`` 19 | @frozen 20 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 21 | public struct ViewEffectConfiguration { 22 | 23 | /// A type-erased content of a ``ViewEffectModifier`` 24 | public struct Content: ViewAlias { } 25 | public var content: Content { .init() } 26 | 27 | /// An opaque identifier to the transaction of a triggered ``ViewEffect`` 28 | public struct ID: Hashable { 29 | var value: UInt 30 | } 31 | public var id: ID 32 | 33 | public var isActive: Bool 34 | public var progress: Double 35 | } 36 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (C) 2022, Nathan Tannar 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in 13 | the documentation and/or other materials provided with the 14 | distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 17 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 18 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 19 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 20 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 21 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 22 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 23 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Sources/Ignition/ViewEffectAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | 7 | /// The animation curves for a ``ViewEffectModifier`` 8 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 9 | @frozen 10 | public struct ViewEffectAnimation: Sendable { 11 | 12 | public var insertion: Animation? 13 | public var removal: Animation? 14 | 15 | public static let `default`: ViewEffectAnimation = .continuous(.linear) 16 | 17 | public static let easeInOut: ViewEffectAnimation = .asymmetric( 18 | insertion: .easeIn.speed(2), 19 | removal: .easeOut.speed(2) 20 | ) 21 | 22 | public static func easeInOut(duration: TimeInterval) -> ViewEffectAnimation { 23 | .asymmetric( 24 | insertion: .easeIn(duration: duration / 2), 25 | removal: .easeOut(duration: duration / 2) 26 | ) 27 | } 28 | 29 | public static func continuous( 30 | _ animation: Animation 31 | ) -> ViewEffectAnimation { 32 | ViewEffectAnimation( 33 | insertion: animation.speed(2), 34 | removal: animation.speed(2) 35 | ) 36 | } 37 | 38 | public static func asymmetric( 39 | insertion: Animation?, 40 | removal: Animation? 41 | ) -> ViewEffectAnimation { 42 | ViewEffectAnimation( 43 | insertion: insertion, 44 | removal: removal 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | runs-on: macos-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Xcode version 18 | run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer 19 | - name: Show available destinations 20 | run: xcodebuild -scheme Ignition -showdestinations 21 | - name: Build for macOS 22 | run: xcodebuild -scheme Ignition -destination 'platform=macOS' build 23 | - name: Build for Catalyst 24 | run: xcodebuild -scheme Ignition -destination 'platform=macOS,variant=Mac Catalyst' build 25 | - name: Build for iOS 26 | run: xcodebuild -scheme Ignition -destination 'platform=iOS Simulator,name=iPhone 16' build 27 | - name: Build for watchOS 28 | run: xcodebuild -scheme Ignition -destination 'platform=watchOS Simulator,name=Apple Watch Ultra 2 (49mm)' build 29 | - name: Build for tvOS 30 | run: xcodebuild -scheme Ignition -destination 'platform=tvOS Simulator,name=Apple TV 4K (3rd generation)' build 31 | - name: Build for visionOS 32 | run: xcodebuild -scheme Ignition -destination 'platform=visionOS Simulator,name=Apple Vision Pro' build 33 | -------------------------------------------------------------------------------- /Sources/Ignition/OpacityEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | 7 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 8 | extension ViewEffect where Self == OpacityEffect { 9 | 10 | /// A ``ViewEffect`` that applies an opacity to the view 11 | public static var opacity: OpacityEffect { 12 | OpacityEffect(opacity: 0) 13 | } 14 | 15 | /// A ``ViewEffect`` that applies an opacity to the view 16 | public static func opacity(_ opacity: Double) -> OpacityEffect { 17 | OpacityEffect(opacity: opacity) 18 | } 19 | } 20 | 21 | /// A ``ViewEffect`` that applies an opacity to the view 22 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 23 | @frozen 24 | public struct OpacityEffect: ViewEffect { 25 | 26 | public var opacity: Double 27 | 28 | @inlinable 29 | public init(opacity: Double) { 30 | self.opacity = opacity 31 | } 32 | 33 | public func makeBody(configuration: Configuration) -> some View { 34 | configuration.content 35 | .opacity(configuration.isActive ? opacity : 1) 36 | } 37 | } 38 | 39 | // MARK: - Previews 40 | 41 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 42 | struct OpacityEffect_Previews: PreviewProvider { 43 | static var previews: some View { 44 | VStack(spacing: 0) { 45 | ForEach(0..<10) { index in 46 | Rectangle() 47 | .fill(.blue) 48 | .scheduledEffect( 49 | .opacity(Double(index) / 10), 50 | interval: 1 51 | ) 52 | } 53 | } 54 | .ignoresSafeArea() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Ignition/BlurEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | 7 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 8 | extension ViewEffect where Self == BlurEffect { 9 | 10 | /// A ``ViewEffect`` that applies a blur to the view 11 | public static func blur( 12 | radius: Double, 13 | opaque: Bool = false 14 | ) -> BlurEffect { 15 | BlurEffect(radius: radius, opaque: opaque) 16 | } 17 | } 18 | 19 | /// A ``ViewEffect`` that applies a blur to the view 20 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 21 | @frozen 22 | public struct BlurEffect: ViewEffect { 23 | 24 | public var radius: Double 25 | public var opaque: Bool = false 26 | 27 | @inlinable 28 | public init(radius: Double, opaque: Bool = false) { 29 | self.radius = radius 30 | self.opaque = opaque 31 | } 32 | 33 | public func makeBody(configuration: Configuration) -> some View { 34 | configuration.content 35 | .blur(radius: configuration.isActive ? radius : 0, opaque: opaque) 36 | } 37 | } 38 | 39 | // MARK: - Previews 40 | 41 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 42 | struct BlurEffect_Previews: PreviewProvider { 43 | static var previews: some View { 44 | VStack(spacing: 24) { 45 | Text("Hello, World") 46 | .scheduledEffect( 47 | .blur(radius: 5), 48 | interval: 1 49 | ) 50 | 51 | Text("Hello, World") 52 | .scheduledEffect( 53 | .blur(radius: 20), 54 | interval: 1 55 | ) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Ignition/ConcatenatedEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | 7 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 8 | extension ViewEffect { 9 | 10 | /// A ``ViewEffect`` that concatenates an additional ``ViewEffect`` 11 | public func concat( 12 | _ effect: ConcatenatingEffect 13 | ) -> ConcatenatedEffect where Self: ViewEffect { 14 | ConcatenatedEffect(effect: self, concatenating: effect) 15 | } 16 | } 17 | 18 | /// A ``ViewEffect`` that concatenates an additional ``ViewEffect`` 19 | @frozen 20 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 21 | public struct ConcatenatedEffect< 22 | Effect: ViewEffect, 23 | ConcatenatingEffect: ViewEffect 24 | >: ViewEffect { 25 | 26 | public let effect: Effect 27 | public let concatenatingEffect: ConcatenatingEffect 28 | 29 | public init( 30 | effect: Effect, 31 | concatenating concatenatingEffect: ConcatenatingEffect 32 | ) { 33 | self.effect = effect 34 | self.concatenatingEffect = concatenatingEffect 35 | } 36 | 37 | public func makeBody(configuration: Configuration) -> some View { 38 | effect.makeBody(configuration: configuration) 39 | .modifier( 40 | ViewEffectModifier( 41 | effect: concatenatingEffect, 42 | configuration: configuration 43 | ) 44 | ) 45 | } 46 | } 47 | 48 | // MARK: - Previews 49 | 50 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 51 | struct ConcatenatedEffect_Previews: PreviewProvider { 52 | static var previews: some View { 53 | VStack(spacing: 24) { 54 | Text("Hello, World") 55 | .scheduledEffect( 56 | .offset.concat(.scale), 57 | interval: 1 58 | ) 59 | 60 | Text("Hello, World") 61 | .scheduledEffect( 62 | .offset(y: 20).concat(.scale(scale: 2)), 63 | interval: 1 64 | ) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Ignition/OffsetEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | 7 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 8 | extension ViewEffect where Self == OffsetEffect { 9 | 10 | /// A ``ViewEffect`` that moves the view between an offset 11 | public static var offset: OffsetEffect { 12 | OffsetEffect(offset: CGPoint(x: 24, y: 0)) 13 | } 14 | 15 | /// A ``ViewEffect`` that moves the view between an offset 16 | public static func offset(x: CGFloat) -> OffsetEffect { 17 | OffsetEffect(offset: CGPoint(x: x, y: 0)) 18 | } 19 | 20 | /// A ``ViewEffect`` that moves the view between an offset 21 | public static func offset(y: CGFloat) -> OffsetEffect { 22 | OffsetEffect(offset: CGPoint(x: 0, y: y)) 23 | } 24 | 25 | /// A ``ViewEffect`` that moves the view between an offset 26 | public static func offset(_ offset: CGPoint) -> OffsetEffect { 27 | OffsetEffect(offset: offset) 28 | } 29 | } 30 | 31 | /// A ``ViewEffect`` that moves the view between an offset 32 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 33 | @frozen 34 | public struct OffsetEffect: ViewEffect { 35 | 36 | public var offset: CGPoint 37 | 38 | @inlinable 39 | public init(offset: CGPoint) { 40 | self.offset = offset 41 | } 42 | 43 | public func makeBody(configuration: Configuration) -> some View { 44 | configuration.content 45 | .modifier( 46 | Effect( 47 | x: configuration.progress * offset.x, 48 | y: configuration.progress * offset.y 49 | ) 50 | .ignoredByLayout() 51 | ) 52 | } 53 | 54 | private struct Effect: GeometryEffect { 55 | var x: CGFloat 56 | var y: CGFloat 57 | 58 | func effectValue(size: CGSize) -> ProjectionTransform { 59 | ProjectionTransform( 60 | CGAffineTransform( 61 | translationX: x, 62 | y: y 63 | ) 64 | ) 65 | } 66 | } 67 | } 68 | 69 | // MARK: - Previews 70 | 71 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 72 | struct OffsetEffect_Previews: PreviewProvider { 73 | static var previews: some View { 74 | VStack(spacing: 24) { 75 | Text("Hello, World") 76 | .scheduledEffect( 77 | .offset, 78 | interval: 1 79 | ) 80 | 81 | Text("Hello, World") 82 | .scheduledEffect( 83 | .offset(y: 20), 84 | interval: 1 85 | ) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Ignition/RotationEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | 7 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 8 | extension ViewEffect where Self == RotationEffect { 9 | 10 | /// A ``ViewEffect`` that rotates the view by an angle 11 | public static var rotate: RotationEffect { 12 | RotationEffect(angle: .degrees(360), anchor: .center) 13 | } 14 | 15 | /// A ``ViewEffect`` that rotates the view by an angle 16 | public static func rotate(angle: Angle, anchor: UnitPoint = .center) -> RotationEffect { 17 | RotationEffect(angle: angle, anchor: anchor) 18 | } 19 | } 20 | 21 | /// A ``ViewEffect`` that rotates the view by an angle 22 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 23 | @frozen 24 | public struct RotationEffect: ViewEffect { 25 | 26 | public var angle: Angle 27 | public var anchor: UnitPoint 28 | 29 | @inlinable 30 | public init(angle: Angle, anchor: UnitPoint) { 31 | self.angle = angle 32 | self.anchor = anchor 33 | } 34 | 35 | public func makeBody(configuration: Configuration) -> some View { 36 | configuration.content 37 | .modifier( 38 | Effect( 39 | angle: configuration.progress * angle.radians, 40 | anchor: anchor 41 | ) 42 | .ignoredByLayout() 43 | ) 44 | } 45 | 46 | private struct Effect: GeometryEffect { 47 | var angle: CGFloat 48 | var anchor: UnitPoint 49 | 50 | func effectValue(size: CGSize) -> ProjectionTransform { 51 | let sin = sin(angle) 52 | let cos = cos(angle) 53 | let x = anchor.x * size.width 54 | let y = anchor.y * size.height 55 | return ProjectionTransform( 56 | CGAffineTransform( 57 | cos, 58 | sin, 59 | -sin, 60 | cos, 61 | x - x * cos + y * sin, 62 | y - x * sin - y * cos 63 | ) 64 | ) 65 | } 66 | } 67 | } 68 | 69 | // MARK: - Previews 70 | 71 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 72 | struct RotationEffect_Previews: PreviewProvider { 73 | static var previews: some View { 74 | VStack(spacing: 24) { 75 | Text("Hello, World") 76 | .scheduledEffect( 77 | .rotate, 78 | interval: 1 79 | ) 80 | 81 | Text("Hello, World") 82 | .scheduledEffect( 83 | .rotate(angle: .degrees(-180)), 84 | interval: 1 85 | ) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Ignition/ScheduledViewEffectModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | import Combine 7 | import Engine 8 | 9 | /// A modifier that adds a ``ViewEffect`` to a view 10 | /// that runs on an interval 11 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 12 | @frozen 13 | public struct ScheduledViewEffectModifier< 14 | Effect: ViewEffect 15 | >: ViewModifier { 16 | 17 | public var effect: Effect 18 | public var interval: TimeInterval 19 | public var animation: ViewEffectAnimation 20 | public var isEnabled: Bool 21 | 22 | @State private var id: UInt = 0 23 | 24 | @_disfavoredOverload 25 | @inlinable 26 | public init( 27 | effect: Effect, 28 | interval: TimeInterval, 29 | animation: Animation, 30 | isEnabled: Bool = true 31 | ) { 32 | self.init( 33 | effect: effect, 34 | interval: interval, 35 | animation: .continuous(animation), 36 | isEnabled: isEnabled 37 | ) 38 | } 39 | 40 | @inlinable 41 | public init( 42 | effect: Effect, 43 | interval: TimeInterval, 44 | animation: ViewEffectAnimation = .default, 45 | isEnabled: Bool = true 46 | ) { 47 | self.effect = effect 48 | self.interval = interval 49 | self.animation = animation 50 | self.isEnabled = isEnabled 51 | } 52 | 53 | public func body(content: Content) -> some View { 54 | content 55 | .changeEffect(effect, value: id, animation: animation, isEnabled: isEnabled) 56 | .onReceive( 57 | Timer.publish(every: interval, on: .main, in: .common).autoconnect() 58 | ) { _ in 59 | if isEnabled { 60 | id = id &+ 1 61 | } 62 | } 63 | } 64 | } 65 | 66 | 67 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 68 | extension View { 69 | 70 | /// Adds a ``ViewEffect`` animation to a view that runs on an interval 71 | @_disfavoredOverload 72 | @inlinable 73 | public func scheduledEffect< 74 | Effect: ViewEffect 75 | >( 76 | _ effect: Effect, 77 | interval: TimeInterval, 78 | animation: Animation, 79 | isEnabled: Bool = true 80 | ) -> some View { 81 | scheduledEffect( 82 | effect, 83 | interval: interval, 84 | animation: .continuous(animation), 85 | isEnabled: isEnabled 86 | ) 87 | } 88 | 89 | /// Adds a ``ViewEffect`` animation to a view that runs on an interval 90 | @inlinable 91 | public func scheduledEffect< 92 | Effect: ViewEffect 93 | >( 94 | _ effect: Effect, 95 | interval: TimeInterval, 96 | animation: ViewEffectAnimation = .default, 97 | isEnabled: Bool = true 98 | ) -> some View { 99 | modifier( 100 | ScheduledViewEffectModifier( 101 | effect: effect, 102 | interval: interval, 103 | animation: animation, 104 | isEnabled: isEnabled 105 | ) 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/Ignition/ScaleEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | 7 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 8 | extension ViewEffect where Self == ScaleEffect { 9 | 10 | /// A ``ViewEffect`` that scales a view between a size 11 | public static var scale: ScaleEffect { 12 | ScaleEffect( 13 | scale: CGSize(width: 1.25, height: 1.25), 14 | anchor: .center 15 | ) 16 | } 17 | 18 | /// A ``ViewEffect`` that scales a view between a size 19 | public static func scale( 20 | width: CGFloat, 21 | anchor: UnitPoint = .center 22 | ) -> ScaleEffect { 23 | ScaleEffect( 24 | scale: CGSize(width: width, height: 1.0), 25 | anchor: anchor 26 | ) 27 | } 28 | 29 | /// A ``ViewEffect`` that scales a view between a size 30 | public static func scale( 31 | height: CGFloat, 32 | anchor: UnitPoint = .center 33 | ) -> ScaleEffect { 34 | ScaleEffect( 35 | scale: CGSize(width: 1.0, height: height), 36 | anchor: anchor 37 | ) 38 | } 39 | 40 | /// A ``ViewEffect`` that scales a view between a size 41 | public static func scale( 42 | scale: CGFloat, 43 | anchor: UnitPoint = .center 44 | ) -> ScaleEffect { 45 | ScaleEffect(scale: CGSize(width: scale, height: scale), anchor: anchor) 46 | } 47 | 48 | /// A ``ViewEffect`` that scales a view between a size 49 | public static func scale( 50 | scale: CGSize, 51 | anchor: UnitPoint = .center 52 | ) -> ScaleEffect { 53 | ScaleEffect(scale: scale, anchor: anchor) 54 | } 55 | } 56 | 57 | /// A ``ViewEffect`` that scales a view between a size 58 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 59 | @frozen 60 | public struct ScaleEffect: ViewEffect { 61 | 62 | public var scale: CGSize 63 | public var anchor: UnitPoint 64 | 65 | @inlinable 66 | public init(scale: CGSize, anchor: UnitPoint) { 67 | self.scale = scale 68 | self.anchor = anchor 69 | } 70 | 71 | public func makeBody(configuration: Configuration) -> some View { 72 | configuration.content 73 | .modifier( 74 | Effect( 75 | x: 1.0 + configuration.progress * (scale.width - 1.0), 76 | y: 1.0 + configuration.progress * (scale.height - 1.0), 77 | anchor: anchor 78 | ) 79 | .ignoredByLayout() 80 | ) 81 | } 82 | 83 | private struct Effect: GeometryEffect { 84 | var x: CGFloat 85 | var y: CGFloat 86 | var anchor: UnitPoint 87 | 88 | func effectValue(size: CGSize) -> ProjectionTransform { 89 | ProjectionTransform( 90 | CGAffineTransform( 91 | translationX: (1 - x) * anchor.x * size.width, 92 | y: (1 - y) * anchor.y * size.height 93 | ) 94 | .scaledBy(x: x, y: y) 95 | ) 96 | } 97 | } 98 | } 99 | 100 | // MARK: - Previews 101 | 102 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 103 | struct ScaleEffect_Previews: PreviewProvider { 104 | static var previews: some View { 105 | VStack(spacing: 24) { 106 | Text("Hello, World") 107 | .scheduledEffect( 108 | .scale, 109 | interval: 1 110 | ) 111 | 112 | Text("Hello, World") 113 | .scheduledEffect( 114 | .scale(scale: 2), 115 | interval: 1 116 | ) 117 | 118 | Text("Hello, World") 119 | .scheduledEffect( 120 | .scale(scale: 0.5), 121 | interval: 1 122 | ) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/Ignition/OverlayEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | import Engine 7 | 8 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 9 | extension ViewEffect { 10 | 11 | /// A ``ViewEffect`` that emits a view in the foreground 12 | public static func overlay( 13 | alignment: Alignment = .center, 14 | content: Content 15 | ) -> OverlayEffect where Self == OverlayEffect { 16 | OverlayEffect( 17 | alignment: alignment, 18 | content: content 19 | ) 20 | } 21 | 22 | /// A ``ViewEffect`` that emits a view in the foreground 23 | public static func overlay( 24 | alignment: Alignment = .center, 25 | @ViewBuilder _ content: () -> Content 26 | ) -> OverlayEffect where Self == OverlayEffect { 27 | OverlayEffect( 28 | alignment: alignment, 29 | content: content() 30 | ) 31 | } 32 | } 33 | 34 | /// A ``ViewEffect`` that emits a view in the foreground 35 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 36 | @frozen 37 | public struct OverlayEffect: ViewEffect { 38 | 39 | public var alignment: Alignment 40 | public var content: Content 41 | 42 | @inlinable 43 | public init( 44 | alignment: Alignment, 45 | content: Content 46 | ) { 47 | self.alignment = alignment 48 | self.content = content 49 | } 50 | 51 | public func makeBody(configuration: Configuration) -> some View { 52 | configuration.content 53 | .overlay( 54 | ViewAdapter { 55 | if configuration.isActive { 56 | content 57 | .id(configuration.id) 58 | } 59 | } 60 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: alignment) 61 | ) 62 | } 63 | } 64 | 65 | // MARK: - Previews 66 | 67 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 68 | struct OverlayEffect_Previews: PreviewProvider { 69 | static var previews: some View { 70 | VStack(spacing: 24) { 71 | Text("Hello, World") 72 | .scheduledEffect( 73 | .overlay { 74 | Rectangle() 75 | .stroke(Color.blue, lineWidth: 2) 76 | .padding(-2) 77 | }, 78 | interval: 1 79 | ) 80 | 81 | Text("Hello, World") 82 | .scheduledEffect( 83 | .overlay { 84 | Rectangle() 85 | .stroke(Color.blue, lineWidth: 2) 86 | .padding(-2) 87 | .transition( 88 | .asymmetric( 89 | insertion: .scale(scale: 0.5), 90 | removal: .scale(scale: 2) 91 | ) 92 | .combined(with: .opacity) 93 | ) 94 | }, 95 | interval: 1 96 | ) 97 | 98 | Text("Hello, World") 99 | .scheduledEffect( 100 | .overlay { 101 | Rectangle() 102 | .stroke(Color.blue, lineWidth: 2) 103 | .padding(-2) 104 | .transition( 105 | .asymmetric( 106 | insertion: .scale(scale: 0.5), 107 | removal: .scale(scale: 2) 108 | ) 109 | .combined(with: .opacity) 110 | ) 111 | }, 112 | interval: 0.05 113 | ) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/Ignition/BackgroundEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | import Engine 7 | 8 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 9 | extension ViewEffect { 10 | 11 | /// A ``ViewEffect`` that emits a view in the background 12 | public static func background( 13 | alignment: Alignment = .center, 14 | content: Content 15 | ) -> BackgroundEffect where Self == BackgroundEffect { 16 | BackgroundEffect( 17 | alignment: alignment, 18 | content: content 19 | ) 20 | } 21 | 22 | /// A ``ViewEffect`` that emits a view in the background 23 | public static func background( 24 | alignment: Alignment = .center, 25 | @ViewBuilder _ content: () -> Content 26 | ) -> BackgroundEffect where Self == BackgroundEffect { 27 | BackgroundEffect( 28 | alignment: alignment, 29 | content: content() 30 | ) 31 | } 32 | } 33 | 34 | /// A ``ViewEffect`` that emits a view in the background 35 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 36 | @frozen 37 | public struct BackgroundEffect: ViewEffect { 38 | 39 | public var alignment: Alignment 40 | public var content: Content 41 | 42 | @inlinable 43 | public init( 44 | alignment: Alignment, 45 | content: Content 46 | ) { 47 | self.alignment = alignment 48 | self.content = content 49 | } 50 | 51 | public func makeBody(configuration: Configuration) -> some View { 52 | configuration.content 53 | .background( 54 | ViewAdapter { 55 | if configuration.isActive { 56 | content 57 | .id(configuration.id) 58 | } 59 | } 60 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: alignment) 61 | ) 62 | } 63 | } 64 | 65 | // MARK: - Previews 66 | 67 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 68 | struct BackgroundEffect_Previews: PreviewProvider { 69 | static var previews: some View { 70 | VStack(spacing: 24) { 71 | Text("Hello, World") 72 | .scheduledEffect( 73 | .background { 74 | Rectangle() 75 | .stroke(Color.blue, lineWidth: 2) 76 | .padding(-2) 77 | }, 78 | interval: 1 79 | ) 80 | 81 | Text("Hello, World") 82 | .scheduledEffect( 83 | .background { 84 | Rectangle() 85 | .stroke(Color.blue, lineWidth: 2) 86 | .padding(-2) 87 | .transition( 88 | .asymmetric( 89 | insertion: .scale(scale: 0.5), 90 | removal: .scale(scale: 2) 91 | ) 92 | .combined(with: .opacity) 93 | ) 94 | }, 95 | interval: 1 96 | ) 97 | 98 | Text("Hello, World") 99 | .scheduledEffect( 100 | .background { 101 | Rectangle() 102 | .stroke(Color.blue, lineWidth: 2) 103 | .padding(-2) 104 | .transition( 105 | .asymmetric( 106 | insertion: .scale(scale: 0.5), 107 | removal: .scale(scale: 2) 108 | ) 109 | .combined(with: .opacity) 110 | ) 111 | }, 112 | interval: 0.05 113 | ) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/Ignition/ChangeEffectButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | import Engine 7 | 8 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 9 | extension PrimitiveButtonStyle { 10 | 11 | /// A ``PrimitiveButtonStyle`` that runs a ``ViewEffect`` when pressed 12 | public static func changeEffect( 13 | effect: Effect 14 | ) -> ChangeEffectButtonStyle where Self == ChangeEffectButtonStyle { 15 | ChangeEffectButtonStyle(effect: effect) 16 | } 17 | 18 | /// A ``PrimitiveButtonStyle`` that runs a ``ViewEffect`` when pressed 19 | public static func changeEffect( 20 | effect: Effect, 21 | animation: Animation 22 | ) -> ChangeEffectButtonStyle where Self == ChangeEffectButtonStyle { 23 | ChangeEffectButtonStyle(effect: effect, animation: animation) 24 | } 25 | } 26 | 27 | /// A ``PrimitiveButtonStyle`` that runs a ``ViewEffect`` when pressed 28 | @frozen 29 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 30 | public struct ChangeEffectButtonStyle< 31 | Effect: ViewEffect 32 | >: PrimitiveButtonStyle { 33 | 34 | public var effect: Effect 35 | public var animation: ViewEffectAnimation 36 | 37 | @State private var trigger: UInt = 0 38 | 39 | @inlinable 40 | public init( 41 | effect: Effect, 42 | animation: Animation 43 | ) { 44 | self.effect = effect 45 | self.animation = .continuous(animation) 46 | } 47 | 48 | @inlinable 49 | public init( 50 | effect: Effect, 51 | animation: ViewEffectAnimation = .default 52 | ) { 53 | self.effect = effect 54 | self.animation = animation 55 | } 56 | 57 | public func makeBody(configuration: Configuration) -> some View { 58 | Button { 59 | trigger &+= 1 60 | configuration.trigger() 61 | } label: { 62 | configuration.label 63 | } 64 | .changeEffect(effect, value: trigger, animation: animation) 65 | } 66 | } 67 | 68 | // MARK: - Previews 69 | 70 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 71 | struct ChangeEffectButtonStyle_Previews: PreviewProvider { 72 | static var previews: some View { 73 | VStack(spacing: 24) { 74 | Button { 75 | 76 | } label: { 77 | Text("Run Effect") 78 | } 79 | .buttonStyle( 80 | .changeEffect( 81 | effect: .background { 82 | RoundedRectangle(cornerRadius: 4) 83 | .stroke(Color.blue, lineWidth: 2) 84 | .transition( 85 | .asymmetric( 86 | insertion: .scale(scale: 0.9), 87 | removal: .scale(scale: 2) 88 | ) 89 | .combined(with: .opacity) 90 | ) 91 | } 92 | ) 93 | ) 94 | 95 | if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { 96 | Button { 97 | 98 | } label: { 99 | Text("Run Effect") 100 | } 101 | .buttonStyle( 102 | .changeEffect( 103 | effect: .background { 104 | RoundedRectangle(cornerRadius: 4) 105 | .stroke(Color.blue, lineWidth: 2) 106 | .transition( 107 | .asymmetric( 108 | insertion: .scale(scale: 0.9), 109 | removal: .scale(scale: 2) 110 | ) 111 | .combined(with: .opacity) 112 | ) 113 | } 114 | ) 115 | ) 116 | .buttonStyle(.bordered) 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Example/Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Example 4 | // 5 | // Created by Nathan Tannar on 2023-09-12. 6 | // 7 | 8 | import SwiftUI 9 | import Ignition 10 | 11 | struct ContentView: View { 12 | @State var counter: Int = 0 13 | @State var isEnabled = true 14 | 15 | var body: some View { 16 | ScrollView(.horizontal, showsIndicators: false) { 17 | HStack(spacing: 48) { 18 | VStack(spacing: 48) { 19 | Text("Ignition") 20 | .font(.title.bold()) 21 | 22 | Text("Schedule Driven") 23 | .foregroundColor(.secondary) 24 | .font(.headline) 25 | 26 | VStack(spacing: 4) { 27 | Text("Event Driven") 28 | .foregroundColor(.secondary) 29 | .font(.headline) 30 | 31 | Button { 32 | counter += 1 33 | } label: { 34 | Text("Run") 35 | .bold() 36 | } 37 | .buttonStyle(.plain) 38 | } 39 | } 40 | 41 | makeEffectPair( 42 | name: "Offset", 43 | shape: Circle(), 44 | effect: .offset, 45 | animation: .asymmetric( 46 | insertion: .linear(duration: 0.05), 47 | removal: .interpolatingSpring(stiffness: 200, damping: 4) 48 | ) 49 | ) 50 | .foregroundColor(.blue) 51 | 52 | makeEffectPair( 53 | name: "Rotate", 54 | shape: Rectangle(), 55 | effect: .rotate(angle: .degrees(10)), 56 | animation: .asymmetric( 57 | insertion: .linear(duration: 0.05), 58 | removal: .interpolatingSpring(stiffness: 200, damping: 4) 59 | ) 60 | ) 61 | .foregroundColor(.yellow) 62 | 63 | makeEffectPair( 64 | name: "Scale", 65 | shape: Circle(), 66 | effect: .scale, 67 | animation: .asymmetric( 68 | insertion: .linear(duration: 0.05), 69 | removal: .interpolatingSpring(stiffness: 200, damping: 4) 70 | ) 71 | ) 72 | .foregroundColor(.green) 73 | 74 | makeEffectPair( 75 | name: "Blur", 76 | shape: Rectangle(), 77 | effect: .blur(radius: 10), 78 | animation: .asymmetric( 79 | insertion: .linear(duration: 0.25), 80 | removal: .linear(duration: 0.25) 81 | ) 82 | ) 83 | .foregroundColor(.purple) 84 | 85 | makeEffectPair( 86 | name: "Opacity", 87 | shape: Circle(), 88 | effect: .opacity, 89 | animation: .asymmetric( 90 | insertion: .linear(duration: 0.25), 91 | removal: .linear(duration: 0.25) 92 | ) 93 | ) 94 | .foregroundColor(.pink) 95 | 96 | makeEffectPair( 97 | name: "Overlay", 98 | shape: Circle(), 99 | effect: .overlay { 100 | Circle() 101 | .stroke(lineWidth: 2) 102 | .transition( 103 | .asymmetric( 104 | insertion: .scale(scale: 0.9), 105 | removal: .scale(scale: 1.5) 106 | ) 107 | .combined(with: .opacity) 108 | ) 109 | }, 110 | animation: .asymmetric( 111 | insertion: nil, 112 | removal: .easeOut(duration: 1) 113 | ) 114 | ) 115 | .foregroundColor(.red) 116 | 117 | makeEffectPair( 118 | name: "Custom", 119 | shape: RoundedRectangle(cornerRadius: 4), 120 | effect: CustomViewEffect { configuration in 121 | configuration.content 122 | .saturation(1 - configuration.progress) 123 | }, 124 | animation: .default 125 | ) 126 | .foregroundColor(.orange) 127 | } 128 | .padding(.vertical, 24) 129 | } 130 | } 131 | 132 | func makeEffectPair< 133 | _Shape: Shape, 134 | Effect: ViewEffect 135 | >( 136 | name: String, 137 | shape: _Shape, 138 | effect: Effect, 139 | animation: ViewEffectAnimation 140 | ) -> some View { 141 | VStack(spacing: 48) { 142 | Text(name) 143 | .font(.headline) 144 | .foregroundColor(.secondary) 145 | 146 | shape 147 | .frame(width: 50, height: 50) 148 | .scheduledEffect( 149 | effect, 150 | interval: 2, 151 | animation: animation, 152 | isEnabled: isEnabled 153 | ) 154 | 155 | shape 156 | .frame(width: 50, height: 50) 157 | .changeEffect( 158 | effect, 159 | value: counter, 160 | animation: animation, 161 | isEnabled: isEnabled 162 | ) 163 | } 164 | } 165 | } 166 | 167 | 168 | struct CustomViewEffect: ViewEffect { 169 | 170 | var content: (Configuration) -> Content 171 | 172 | init( 173 | @ViewBuilder content: @escaping (Configuration) -> Content 174 | ) { 175 | self.content = content 176 | } 177 | 178 | func makeBody(configuration: Configuration) -> some View { 179 | content(configuration) 180 | } 181 | } 182 | 183 | 184 | struct ContentView_Previews: PreviewProvider { 185 | static var previews: some View { 186 | ContentView() 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Ignition 4 | 5 | `Ignition` aims to help make your SwiftUI views feel more interactive. It does this by providing API that makes it easier to run animations. 6 | 7 | > Built for performance and backwards compatibility using [Engine](https://github.com/nathantannar4/Engine) 8 | 9 | ## See Also 10 | 11 | - [Turbocharger](https://github.com/nathantannar4/Turbocharger) 12 | - [Transmission](https://github.com/nathantannar4/Transmission) 13 | 14 | ## Preview 15 | 16 | https://github.com/nathantannar4/Ignition/assets/15272998/0d7b97a0-bf3a-4c07-9a00-b237408f49a4 17 | 18 | ## Requirements 19 | 20 | - Deployment target: iOS 13.0, macOS 10.15, tvOS 13.0, or watchOS 6.0 21 | - Xcode 15+ 22 | 23 | ## Installation 24 | 25 | ### Xcode Projects 26 | 27 | Select `File` -> `Swift Packages` -> `Add Package Dependency` and enter `https://github.com/nathantannar4/Ignition`. 28 | 29 | ### Swift Package Manager Projects 30 | 31 | You can add `Ignition` as a package dependency in your `Package.swift` file: 32 | 33 | ```swift 34 | let package = Package( 35 | //... 36 | dependencies: [ 37 | .package(url: "https://github.com/nathantannar4/Ignition"), 38 | ], 39 | targets: [ 40 | .target( 41 | name: "YourPackageTarget", 42 | dependencies: [ 43 | .product(name: "Ignition", package: "Ignition"), 44 | ], 45 | //... 46 | ), 47 | //... 48 | ], 49 | //... 50 | ) 51 | ``` 52 | 53 | ## Documentation 54 | 55 | Detailed documentation is available [here](https://swiftpackageindex.com/nathantannar4/Ignition/main/documentation/ignition). 56 | 57 | ## Effects 58 | 59 | `Ignition` provides 5 built in effects, but you can also make your own by conforming to the `ViewEffect` protocol. 60 | 61 | ```swift 62 | /// The style for ``ViewEffectModifier`` 63 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 64 | public protocol ViewEffect: DynamicProperty { 65 | 66 | associatedtype Body: View 67 | @MainActor @ViewBuilder func makeBody(configuration: Configuration) -> Body 68 | 69 | typealias Configuration = ViewEffectConfiguration 70 | } 71 | 72 | /// The configuration parameters for ``ViewEffect`` 73 | @frozen 74 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 75 | public struct ViewEffectConfiguration { 76 | 77 | /// A type-erased content of a ``ViewEffectModifier`` 78 | public struct Content: ViewAlias { } 79 | public var content: Content 80 | 81 | /// An opaque identifier to the transaction of a triggered ``ViewEffect`` 82 | public struct ID: Hashable { } 83 | public var id: ID 84 | 85 | public var isActive: Bool 86 | public var progress: Double 87 | } 88 | ``` 89 | 90 | ### Concatenating Effects 91 | 92 | Multiple effects can be concatenating together, for example: `.scale.concat(.offset)` 93 | 94 | ```swift 95 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 96 | extension ViewEffect { 97 | 98 | /// A ``ViewEffect`` that concatenates an additional ``ViewEffect`` 99 | public func concat( 100 | _ effect: ConcatenatingEffect 101 | ) -> ConcatenatedEffect where Self: ViewEffect 102 | } 103 | ``` 104 | 105 | ### Triggering Effects 106 | 107 | Effects can be triggered via the `OnChangeViewEffectModifier` or the `ScheduledViewEffectModifier`. 108 | 109 | ```swift 110 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 111 | extension View { 112 | 113 | /// Adds a ``ViewEffect`` animation to a view that runs when the value changes 114 | @_disfavoredOverload 115 | @inlinable 116 | public func changeEffect< 117 | Effect: ViewEffect, 118 | Value: Equatable 119 | >( 120 | _ effect: Effect, 121 | value: Value, 122 | animation: Animation, 123 | isEnabled: Bool = true 124 | ) -> some View 125 | 126 | /// Adds a ``ViewEffect`` animation to a view that runs on an interval 127 | @_disfavoredOverload 128 | @inlinable 129 | public func scheduledEffect< 130 | Effect: ViewEffect 131 | >( 132 | _ effect: Effect, 133 | interval: TimeInterval, 134 | animation: Animation, 135 | isEnabled: Bool = true 136 | ) -> some View { 137 | scheduledEffect( 138 | effect, 139 | interval: interval, 140 | animation: .continuous(animation), 141 | isEnabled: isEnabled 142 | ) 143 | } 144 | } 145 | ``` 146 | 147 | ### BackgroundEffect 148 | 149 | ```swift 150 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 151 | extension ViewEffect { 152 | 153 | /// A ``ViewEffect`` that emits a view in the background 154 | public static func background( 155 | alignment: Alignment = .center, 156 | @ViewBuilder _ content: () -> Content 157 | ) -> BackgroundEffect where Self == BackgroundEffect 158 | } 159 | ``` 160 | 161 | ### OverlayEffect 162 | 163 | ```swift 164 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 165 | extension ViewEffect { 166 | 167 | /// A ``ViewEffect`` that emits a view in the foreground 168 | public static func overlay( 169 | alignment: Alignment = .center, 170 | @ViewBuilder _ content: () -> Content 171 | ) -> OverlayEffect where Self == OverlayEffect 172 | } 173 | ``` 174 | 175 | ### OffsetEffect 176 | 177 | ```swift 178 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 179 | extension ViewEffect where Self == OffsetEffect { 180 | 181 | /// A ``ViewEffect`` that moves the view between an offset 182 | public static var offset: OffsetEffect 183 | 184 | /// A ``ViewEffect`` that moves the view between an offset 185 | public static func offset(x: CGFloat) -> OffsetEffect 186 | 187 | /// A ``ViewEffect`` that moves the view between an offset 188 | public static func offset(y: CGFloat) -> OffsetEffect 189 | 190 | /// A ``ViewEffect`` that moves the view between an offset 191 | public static func offset(offset: CGPoint) -> OffsetEffect 192 | } 193 | ``` 194 | 195 | ### RotationEffect 196 | 197 | ```swift 198 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 199 | extension ViewEffect where Self == RotationEffect { 200 | 201 | /// A ``ViewEffect`` that rotates the view by an angle 202 | public static var rotate: RotationEffect 203 | 204 | /// A ``ViewEffect`` that rotates the view by an angle 205 | public static func rotate(angle: Angle, anchor: UnitPoint = .center) -> RotationEffect 206 | } 207 | ``` 208 | 209 | ### ScaleEffect 210 | 211 | ```swift 212 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 213 | extension ViewEffect where Self == ScaleEffect { 214 | 215 | /// A ``ViewEffect`` that scales a view between a size 216 | public static var scale: ScaleEffect 217 | 218 | /// A ``ViewEffect`` that scales a view between a size 219 | public static func scale(width: CGFloat, anchor: UnitPoint = .center) -> ScaleEffect 220 | 221 | /// A ``ViewEffect`` that scales a view between a size 222 | public static func scale(height: CGFloat, anchor: UnitPoint = .center) -> ScaleEffect 223 | 224 | /// A ``ViewEffect`` that scales a view between a size 225 | public static func scale(scale: CGFloat, anchor: UnitPoint = .center) -> ScaleEffect 226 | 227 | /// A ``ViewEffect`` that scales a view between a size 228 | public static func scale(scale: CGSize, anchor: UnitPoint = .center) -> ScaleEffect 229 | } 230 | ``` 231 | 232 | ### ButtonStyle 233 | 234 | Want to run an effect when a user presses a button? Try using the `.changeEffect` button style. 235 | 236 | ```swift 237 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 238 | extension PrimitiveButtonStyle { 239 | 240 | /// A ``PrimitiveButtonStyle`` that runs a ``ViewEffect`` when pressed 241 | public static func changeEffect( 242 | effect: Effect 243 | ) -> ChangeEffectButtonStyle where Self == ChangeEffectButtonStyle 244 | } 245 | ``` 246 | -------------------------------------------------------------------------------- /Sources/Ignition/OnChangeViewEffectModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Nathan Tannar 3 | // 4 | 5 | import SwiftUI 6 | import Engine 7 | 8 | /// A modifier that adds a ``ViewEffect`` to a view 9 | /// that runs when the value changes 10 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 11 | @frozen 12 | public struct OnChangeViewEffectModifier< 13 | Effect: ViewEffect, 14 | Value: Equatable 15 | >: ViewModifier { 16 | 17 | public var effect: Effect 18 | public var value: Value 19 | public var animation: ViewEffectAnimation 20 | public var isEnabled: Bool 21 | 22 | @State private var id: UInt = 0 23 | @State private var isActive = false 24 | 25 | @_disfavoredOverload 26 | @inlinable 27 | public init( 28 | effect: Effect, 29 | value: Value, 30 | animation: Animation, 31 | isEnabled: Bool = true 32 | ) { 33 | self.init( 34 | effect: effect, 35 | value: value, 36 | animation: .continuous(animation), 37 | isEnabled: isEnabled 38 | ) 39 | } 40 | 41 | @inlinable 42 | public init( 43 | effect: Effect, 44 | value: Value, 45 | animation: ViewEffectAnimation = .default, 46 | isEnabled: Bool = true 47 | ) { 48 | self.effect = effect 49 | self.value = value 50 | self.animation = animation 51 | self.isEnabled = isEnabled 52 | } 53 | 54 | public func body(content: Content) -> some View { 55 | let animation = (isActive ? animation.insertion : animation.removal) ?? .linear(duration: 0) 56 | content 57 | .modifier( 58 | OnChangeViewEffectModifierBody( 59 | effect: effect, 60 | isActive: $isActive, 61 | id: id 62 | ) 63 | .animation(animation) 64 | ) 65 | .onChange(of: value) { _ in 66 | if isEnabled { 67 | id = id &+ 1 68 | isActive = true 69 | } 70 | } 71 | } 72 | } 73 | 74 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 75 | extension View { 76 | 77 | /// Adds a ``ViewEffect`` animation to a view that runs when the value changes 78 | @_disfavoredOverload 79 | @inlinable 80 | public func changeEffect< 81 | Effect: ViewEffect, 82 | Value: Equatable 83 | >( 84 | _ effect: Effect, 85 | value: Value, 86 | animation: Animation, 87 | isEnabled: Bool = true 88 | ) -> some View { 89 | changeEffect( 90 | effect, 91 | value: value, 92 | animation: .continuous(animation), 93 | isEnabled: isEnabled 94 | ) 95 | } 96 | 97 | /// Adds a ``ViewEffect`` animation to a view that runs when the value changes 98 | @inlinable 99 | public func changeEffect< 100 | Effect: ViewEffect, 101 | Value: Equatable 102 | >( 103 | _ effect: Effect, 104 | value: Value, 105 | animation: ViewEffectAnimation = .default, 106 | isEnabled: Bool = true 107 | ) -> some View { 108 | modifier( 109 | OnChangeViewEffectModifier( 110 | effect: effect, 111 | value: value, 112 | animation: animation, 113 | isEnabled: isEnabled 114 | ) 115 | ) 116 | } 117 | } 118 | 119 | // Using AnimatableModifier due to: 120 | // FB13095046 - Animation behaviour broken for Animatable when type imported from framework 121 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 122 | private struct OnChangeViewEffectModifierBody< 123 | Effect: ViewEffect 124 | >: AnimatableModifier { 125 | 126 | var effect: Effect 127 | var isActive: Binding 128 | var id: UInt 129 | var progress: Double 130 | 131 | nonisolated var animatableData: Double { 132 | get { progress } 133 | set { progress = newValue } 134 | } 135 | 136 | private struct OnChangeValue: Equatable { 137 | var isComplete: Bool 138 | var id: UInt 139 | } 140 | 141 | init( 142 | effect: Effect, 143 | isActive: Binding, 144 | id: UInt 145 | ) { 146 | self.effect = effect 147 | self.isActive = isActive 148 | self.id = id 149 | self.progress = isActive.wrappedValue ? 1 : 0 150 | } 151 | 152 | func body(content: Content) -> some View { 153 | content 154 | .modifier( 155 | ViewEffectModifier( 156 | effect: effect, 157 | configuration: ViewEffectConfiguration( 158 | id: ViewEffectConfiguration.ID(value: id), 159 | isActive: isActive.wrappedValue, 160 | progress: progress 161 | ) 162 | ) 163 | .transaction { $0.disablesAnimations = true } 164 | ) 165 | .onChange( 166 | of: OnChangeValue(isComplete: progress >= 1, id: id) 167 | ) { value in 168 | if value.isComplete { 169 | isActive.wrappedValue = false 170 | } 171 | } 172 | } 173 | } 174 | 175 | /* 176 | This approach works for some types of effects, but not effects that 177 | generate views such as .overlay and .background. Moreover, it does 178 | not allow the animation curve to be different between presentation 179 | and dismissal. 180 | 181 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 182 | @frozen 183 | public struct OnChangeViewEffectModifier< 184 | Effect: ViewEffect, 185 | Value: Equatable 186 | >: ViewModifier { 187 | 188 | public var effect: Effect 189 | public var value: Value 190 | public var animation: Animation 191 | public var isEnabled: Bool 192 | 193 | @State private var id: UInt = 0 194 | @State private var isActive: Bool = false 195 | @State private var trigger: Bool = false 196 | 197 | @inlinable 198 | public init( 199 | effect: Effect, 200 | value: Value, 201 | animation: Animation = .default, 202 | isEnabled: Bool = true 203 | ) { 204 | self.effect = effect 205 | self.value = value 206 | self.animation = animation 207 | self.isEnabled = isEnabled 208 | } 209 | 210 | public func body(content: Content) -> some View { 211 | content 212 | .modifier( 213 | OnChangeViewEffectModifierBody( 214 | effect: effect, 215 | trigger: trigger, 216 | id: id 217 | ) 218 | .animation(animation) 219 | ) 220 | .onChange(of: value) { _ in 221 | if isEnabled { 222 | id = id &+ 1 223 | trigger.toggle() 224 | isActive = true 225 | } 226 | } 227 | } 228 | } 229 | 230 | // Using AnimatableModifier due to: 231 | // FB13095046 - Animation behaviour broken for Animatable when type imported from framework 232 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 233 | private struct OnChangeViewEffectModifierBody< 234 | Effect: ViewEffect 235 | >: AnimatableModifier { 236 | 237 | var effect: Effect 238 | var trigger: Bool 239 | var id: UInt 240 | var progress: Double 241 | 242 | var animatableData: Double { 243 | get { progress } 244 | set { progress = newValue } 245 | } 246 | 247 | var configuration: ViewEffectConfiguration { 248 | let isActive = trigger ? progress <= 0.5 : progress > 0.5 249 | var progress = min(max(self.progress, 0), 1) 250 | if !trigger { 251 | progress = 1 - progress 252 | } 253 | if isActive { 254 | progress = progress * 2 255 | } else { 256 | progress = 1 - (progress - 0.5) * 2 257 | } 258 | return ViewEffectConfiguration( 259 | id: ViewEffectConfiguration.ID(value: id), 260 | isActive: isActive, 261 | progress: progress 262 | ) 263 | } 264 | 265 | init( 266 | effect: Effect, 267 | trigger: Bool, 268 | id: UInt 269 | ) { 270 | self.effect = effect 271 | self.trigger = trigger 272 | self.id = id 273 | self.progress = trigger ? 1 : 0 274 | } 275 | 276 | public func body(content: Content) -> some View { 277 | content 278 | .modifier( 279 | ViewEffectModifier( 280 | effect: effect, 281 | configuration: configuration 282 | ) 283 | .transaction { $0.disablesAnimations = true } 284 | ) 285 | } 286 | } 287 | */ 288 | 289 | // MARK: - Previews 290 | 291 | #if os(iOS) || os(macOS) 292 | 293 | @available(iOS 14.0, macOS 11.0, *) 294 | struct OnChangeViewEffectModifier_Previews: PreviewProvider { 295 | struct PreviewEffect: ViewEffect { 296 | func makeBody(configuration: Configuration) -> some View { 297 | configuration.content 298 | .onChange(of: configuration.progress) { newValue in 299 | print(newValue) 300 | } 301 | } 302 | } 303 | 304 | struct Preview: View { 305 | @State var trigger = 0 306 | 307 | var body: some View { 308 | VStack { 309 | ZStack { 310 | Color.blue 311 | 312 | Text("Trigger") 313 | .foregroundColor(.white) 314 | } 315 | .frame(width: 100, height: 100) 316 | .changeEffect( 317 | .opacity, 318 | value: trigger 319 | ) 320 | 321 | ZStack { 322 | Color.blue 323 | 324 | Text("Trigger") 325 | .foregroundColor(.white) 326 | } 327 | .frame(width: 100, height: 100) 328 | .changeEffect( 329 | .background { 330 | Rectangle() 331 | .stroke(Color.blue, lineWidth: 2) 332 | .padding(-2) 333 | .transition( 334 | .asymmetric( 335 | insertion: .scale(scale: 0.5), 336 | removal: .scale(scale: 2) 337 | ) 338 | .combined(with: .opacity) 339 | ) 340 | }, 341 | value: trigger 342 | ) 343 | } 344 | .onTapGesture { 345 | trigger += 1 346 | } 347 | } 348 | } 349 | 350 | static var previews: some View { 351 | Preview() 352 | } 353 | } 354 | 355 | #endif 356 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 383F0CF32AB1321200DFBA12 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383F0CF22AB1321200DFBA12 /* ExampleApp.swift */; }; 11 | 383F0CF52AB1321200DFBA12 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383F0CF42AB1321200DFBA12 /* ContentView.swift */; }; 12 | 383F0CF72AB1321300DFBA12 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 383F0CF62AB1321300DFBA12 /* Assets.xcassets */; }; 13 | 383F0CFB2AB1321300DFBA12 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 383F0CFA2AB1321300DFBA12 /* Preview Assets.xcassets */; }; 14 | 383F0D052ABBB4DF00DFBA12 /* Ignition in Frameworks */ = {isa = PBXBuildFile; productRef = 383F0D042ABBB4DF00DFBA12 /* Ignition */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | 383F0CEF2AB1321200DFBA12 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | 383F0CF22AB1321200DFBA12 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; 20 | 383F0CF42AB1321200DFBA12 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 21 | 383F0CF62AB1321300DFBA12 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 22 | 383F0CF82AB1321300DFBA12 /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; }; 23 | 383F0CFA2AB1321300DFBA12 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 24 | 383F0D022AB1324300DFBA12 /* Ignition */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Ignition; path = ..; sourceTree = ""; }; 25 | /* End PBXFileReference section */ 26 | 27 | /* Begin PBXFrameworksBuildPhase section */ 28 | 383F0CEC2AB1321200DFBA12 /* Frameworks */ = { 29 | isa = PBXFrameworksBuildPhase; 30 | buildActionMask = 2147483647; 31 | files = ( 32 | 383F0D052ABBB4DF00DFBA12 /* Ignition in Frameworks */, 33 | ); 34 | runOnlyForDeploymentPostprocessing = 0; 35 | }; 36 | /* End PBXFrameworksBuildPhase section */ 37 | 38 | /* Begin PBXGroup section */ 39 | 383F0CE62AB1321200DFBA12 = { 40 | isa = PBXGroup; 41 | children = ( 42 | 383F0D012AB1324300DFBA12 /* Packages */, 43 | 383F0CF12AB1321200DFBA12 /* Example */, 44 | 383F0CF02AB1321200DFBA12 /* Products */, 45 | 383F0D032ABBB4DF00DFBA12 /* Frameworks */, 46 | ); 47 | sourceTree = ""; 48 | }; 49 | 383F0CF02AB1321200DFBA12 /* Products */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 383F0CEF2AB1321200DFBA12 /* Example.app */, 53 | ); 54 | name = Products; 55 | sourceTree = ""; 56 | }; 57 | 383F0CF12AB1321200DFBA12 /* Example */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 383F0CF22AB1321200DFBA12 /* ExampleApp.swift */, 61 | 383F0CF42AB1321200DFBA12 /* ContentView.swift */, 62 | 383F0CF62AB1321300DFBA12 /* Assets.xcassets */, 63 | 383F0CF82AB1321300DFBA12 /* Example.entitlements */, 64 | 383F0CF92AB1321300DFBA12 /* Preview Content */, 65 | ); 66 | path = Example; 67 | sourceTree = ""; 68 | }; 69 | 383F0CF92AB1321300DFBA12 /* Preview Content */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 383F0CFA2AB1321300DFBA12 /* Preview Assets.xcassets */, 73 | ); 74 | path = "Preview Content"; 75 | sourceTree = ""; 76 | }; 77 | 383F0D012AB1324300DFBA12 /* Packages */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | 383F0D022AB1324300DFBA12 /* Ignition */, 81 | ); 82 | name = Packages; 83 | sourceTree = ""; 84 | }; 85 | 383F0D032ABBB4DF00DFBA12 /* Frameworks */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | ); 89 | name = Frameworks; 90 | sourceTree = ""; 91 | }; 92 | /* End PBXGroup section */ 93 | 94 | /* Begin PBXNativeTarget section */ 95 | 383F0CEE2AB1321200DFBA12 /* Example */ = { 96 | isa = PBXNativeTarget; 97 | buildConfigurationList = 383F0CFE2AB1321300DFBA12 /* Build configuration list for PBXNativeTarget "Example" */; 98 | buildPhases = ( 99 | 383F0CEB2AB1321200DFBA12 /* Sources */, 100 | 383F0CEC2AB1321200DFBA12 /* Frameworks */, 101 | 383F0CED2AB1321200DFBA12 /* Resources */, 102 | ); 103 | buildRules = ( 104 | ); 105 | dependencies = ( 106 | ); 107 | name = Example; 108 | packageProductDependencies = ( 109 | 383F0D042ABBB4DF00DFBA12 /* Ignition */, 110 | ); 111 | productName = Example; 112 | productReference = 383F0CEF2AB1321200DFBA12 /* Example.app */; 113 | productType = "com.apple.product-type.application"; 114 | }; 115 | /* End PBXNativeTarget section */ 116 | 117 | /* Begin PBXProject section */ 118 | 383F0CE72AB1321200DFBA12 /* Project object */ = { 119 | isa = PBXProject; 120 | attributes = { 121 | BuildIndependentTargetsInParallel = 1; 122 | LastSwiftUpdateCheck = 1430; 123 | LastUpgradeCheck = 1430; 124 | TargetAttributes = { 125 | 383F0CEE2AB1321200DFBA12 = { 126 | CreatedOnToolsVersion = 14.3.1; 127 | }; 128 | }; 129 | }; 130 | buildConfigurationList = 383F0CEA2AB1321200DFBA12 /* Build configuration list for PBXProject "Example" */; 131 | compatibilityVersion = "Xcode 14.0"; 132 | developmentRegion = en; 133 | hasScannedForEncodings = 0; 134 | knownRegions = ( 135 | en, 136 | Base, 137 | ); 138 | mainGroup = 383F0CE62AB1321200DFBA12; 139 | productRefGroup = 383F0CF02AB1321200DFBA12 /* Products */; 140 | projectDirPath = ""; 141 | projectRoot = ""; 142 | targets = ( 143 | 383F0CEE2AB1321200DFBA12 /* Example */, 144 | ); 145 | }; 146 | /* End PBXProject section */ 147 | 148 | /* Begin PBXResourcesBuildPhase section */ 149 | 383F0CED2AB1321200DFBA12 /* Resources */ = { 150 | isa = PBXResourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | 383F0CFB2AB1321300DFBA12 /* Preview Assets.xcassets in Resources */, 154 | 383F0CF72AB1321300DFBA12 /* Assets.xcassets in Resources */, 155 | ); 156 | runOnlyForDeploymentPostprocessing = 0; 157 | }; 158 | /* End PBXResourcesBuildPhase section */ 159 | 160 | /* Begin PBXSourcesBuildPhase section */ 161 | 383F0CEB2AB1321200DFBA12 /* Sources */ = { 162 | isa = PBXSourcesBuildPhase; 163 | buildActionMask = 2147483647; 164 | files = ( 165 | 383F0CF52AB1321200DFBA12 /* ContentView.swift in Sources */, 166 | 383F0CF32AB1321200DFBA12 /* ExampleApp.swift in Sources */, 167 | ); 168 | runOnlyForDeploymentPostprocessing = 0; 169 | }; 170 | /* End PBXSourcesBuildPhase section */ 171 | 172 | /* Begin XCBuildConfiguration section */ 173 | 383F0CFC2AB1321300DFBA12 /* Debug */ = { 174 | isa = XCBuildConfiguration; 175 | buildSettings = { 176 | ALWAYS_SEARCH_USER_PATHS = NO; 177 | CLANG_ANALYZER_NONNULL = YES; 178 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 179 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 180 | CLANG_ENABLE_MODULES = YES; 181 | CLANG_ENABLE_OBJC_ARC = YES; 182 | CLANG_ENABLE_OBJC_WEAK = YES; 183 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 184 | CLANG_WARN_BOOL_CONVERSION = YES; 185 | CLANG_WARN_COMMA = YES; 186 | CLANG_WARN_CONSTANT_CONVERSION = YES; 187 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 188 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 189 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 190 | CLANG_WARN_EMPTY_BODY = YES; 191 | CLANG_WARN_ENUM_CONVERSION = YES; 192 | CLANG_WARN_INFINITE_RECURSION = YES; 193 | CLANG_WARN_INT_CONVERSION = YES; 194 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 195 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 196 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 197 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 198 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 199 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 200 | CLANG_WARN_STRICT_PROTOTYPES = YES; 201 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 202 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 203 | CLANG_WARN_UNREACHABLE_CODE = YES; 204 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 205 | COPY_PHASE_STRIP = NO; 206 | DEBUG_INFORMATION_FORMAT = dwarf; 207 | ENABLE_STRICT_OBJC_MSGSEND = YES; 208 | ENABLE_TESTABILITY = YES; 209 | GCC_C_LANGUAGE_STANDARD = gnu11; 210 | GCC_DYNAMIC_NO_PIC = NO; 211 | GCC_NO_COMMON_BLOCKS = YES; 212 | GCC_OPTIMIZATION_LEVEL = 0; 213 | GCC_PREPROCESSOR_DEFINITIONS = ( 214 | "DEBUG=1", 215 | "$(inherited)", 216 | ); 217 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 218 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 219 | GCC_WARN_UNDECLARED_SELECTOR = YES; 220 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 221 | GCC_WARN_UNUSED_FUNCTION = YES; 222 | GCC_WARN_UNUSED_VARIABLE = YES; 223 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 224 | MTL_FAST_MATH = YES; 225 | ONLY_ACTIVE_ARCH = YES; 226 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 227 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 228 | }; 229 | name = Debug; 230 | }; 231 | 383F0CFD2AB1321300DFBA12 /* Release */ = { 232 | isa = XCBuildConfiguration; 233 | buildSettings = { 234 | ALWAYS_SEARCH_USER_PATHS = NO; 235 | CLANG_ANALYZER_NONNULL = YES; 236 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 237 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 238 | CLANG_ENABLE_MODULES = YES; 239 | CLANG_ENABLE_OBJC_ARC = YES; 240 | CLANG_ENABLE_OBJC_WEAK = YES; 241 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 242 | CLANG_WARN_BOOL_CONVERSION = YES; 243 | CLANG_WARN_COMMA = YES; 244 | CLANG_WARN_CONSTANT_CONVERSION = YES; 245 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 246 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 247 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 248 | CLANG_WARN_EMPTY_BODY = YES; 249 | CLANG_WARN_ENUM_CONVERSION = YES; 250 | CLANG_WARN_INFINITE_RECURSION = YES; 251 | CLANG_WARN_INT_CONVERSION = YES; 252 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 253 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 254 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 256 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 261 | CLANG_WARN_UNREACHABLE_CODE = YES; 262 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 263 | COPY_PHASE_STRIP = NO; 264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 265 | ENABLE_NS_ASSERTIONS = NO; 266 | ENABLE_STRICT_OBJC_MSGSEND = YES; 267 | GCC_C_LANGUAGE_STANDARD = gnu11; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | MTL_ENABLE_DEBUG_INFO = NO; 276 | MTL_FAST_MATH = YES; 277 | SWIFT_COMPILATION_MODE = wholemodule; 278 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 279 | }; 280 | name = Release; 281 | }; 282 | 383F0CFF2AB1321300DFBA12 /* Debug */ = { 283 | isa = XCBuildConfiguration; 284 | buildSettings = { 285 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 286 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 287 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 288 | CODE_SIGN_STYLE = Automatic; 289 | CURRENT_PROJECT_VERSION = 1; 290 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 291 | DEVELOPMENT_TEAM = JH5XJ55XGZ; 292 | ENABLE_HARDENED_RUNTIME = YES; 293 | ENABLE_PREVIEWS = YES; 294 | GENERATE_INFOPLIST_FILE = YES; 295 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 296 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 297 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 298 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 299 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 300 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 301 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 302 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 303 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 304 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 305 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 306 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 307 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 308 | MACOSX_DEPLOYMENT_TARGET = 13.3; 309 | MARKETING_VERSION = 1.0; 310 | PRODUCT_BUNDLE_IDENTIFIER = com.nathantannar.Example; 311 | PRODUCT_NAME = "$(TARGET_NAME)"; 312 | SDKROOT = auto; 313 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 314 | SWIFT_EMIT_LOC_STRINGS = YES; 315 | SWIFT_VERSION = 5.0; 316 | TARGETED_DEVICE_FAMILY = "1,2"; 317 | }; 318 | name = Debug; 319 | }; 320 | 383F0D002AB1321300DFBA12 /* Release */ = { 321 | isa = XCBuildConfiguration; 322 | buildSettings = { 323 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 324 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 325 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 326 | CODE_SIGN_STYLE = Automatic; 327 | CURRENT_PROJECT_VERSION = 1; 328 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 329 | DEVELOPMENT_TEAM = JH5XJ55XGZ; 330 | ENABLE_HARDENED_RUNTIME = YES; 331 | ENABLE_PREVIEWS = YES; 332 | GENERATE_INFOPLIST_FILE = YES; 333 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 334 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 335 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 336 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 337 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 338 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 339 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 340 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 341 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 342 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 343 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 344 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 345 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 346 | MACOSX_DEPLOYMENT_TARGET = 13.3; 347 | MARKETING_VERSION = 1.0; 348 | PRODUCT_BUNDLE_IDENTIFIER = com.nathantannar.Example; 349 | PRODUCT_NAME = "$(TARGET_NAME)"; 350 | SDKROOT = auto; 351 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 352 | SWIFT_EMIT_LOC_STRINGS = YES; 353 | SWIFT_VERSION = 5.0; 354 | TARGETED_DEVICE_FAMILY = "1,2"; 355 | }; 356 | name = Release; 357 | }; 358 | /* End XCBuildConfiguration section */ 359 | 360 | /* Begin XCConfigurationList section */ 361 | 383F0CEA2AB1321200DFBA12 /* Build configuration list for PBXProject "Example" */ = { 362 | isa = XCConfigurationList; 363 | buildConfigurations = ( 364 | 383F0CFC2AB1321300DFBA12 /* Debug */, 365 | 383F0CFD2AB1321300DFBA12 /* Release */, 366 | ); 367 | defaultConfigurationIsVisible = 0; 368 | defaultConfigurationName = Release; 369 | }; 370 | 383F0CFE2AB1321300DFBA12 /* Build configuration list for PBXNativeTarget "Example" */ = { 371 | isa = XCConfigurationList; 372 | buildConfigurations = ( 373 | 383F0CFF2AB1321300DFBA12 /* Debug */, 374 | 383F0D002AB1321300DFBA12 /* Release */, 375 | ); 376 | defaultConfigurationIsVisible = 0; 377 | defaultConfigurationName = Release; 378 | }; 379 | /* End XCConfigurationList section */ 380 | 381 | /* Begin XCSwiftPackageProductDependency section */ 382 | 383F0D042ABBB4DF00DFBA12 /* Ignition */ = { 383 | isa = XCSwiftPackageProductDependency; 384 | productName = Ignition; 385 | }; 386 | /* End XCSwiftPackageProductDependency section */ 387 | }; 388 | rootObject = 383F0CE72AB1321200DFBA12 /* Project object */; 389 | } 390 | --------------------------------------------------------------------------------