├── 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 |
--------------------------------------------------------------------------------