├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Package.swift
├── README.md
├── Sources
└── ParallaxSwiftUI
│ └── ParallaxSwiftUI.swift
├── preview.gif
└── tutorial.png
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "ParallaxSwiftUI",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, and make them visible to other packages.
13 | .library(
14 | name: "ParallaxSwiftUI",
15 | targets: ["ParallaxSwiftUI"]),
16 | ],
17 | targets: [
18 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
19 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
20 | .target(
21 | name: "ParallaxSwiftUI",
22 | dependencies: []),
23 | ]
24 | )
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ParallaxSwiftUI
2 |
3 | Add some depth to your SwiftUI interface with ParallaxSwiftUI. It uses motion and your devices sensors to create a parallax effect.
4 | It's super easy to use and customise.
5 |
6 | ## Installation
7 |
8 | **Using Swift Package Manager:**
9 | https://github.com/Priva28/ParallaxSwiftUI
10 |
11 | Or you can just copy the `ParallaxSwiftUI.swift` file to your project.
12 |
13 | ## How to use.
14 |
15 | This is all you need to add a parallax effect to any SwiftUI view:
16 |
17 | ```swift
18 | .parallax()
19 | ```
20 |
21 | Really, that's it.
22 |
23 | ## How to customize.
24 |
25 | Right now you can customise the direction the parallax occurs in, and the amount of the effect.
26 |
27 | **To set the direction do this:**
28 |
29 | ```swift
30 | .parallax(direction: .vertical)
31 | ```
32 |
33 | You can use `.vertical`, `.horizontal` or `.both` which is the default.
34 |
35 | **To set the amount do this:**
36 |
37 | ```swift
38 | .parallax(amount: 20)
39 | ```
40 |
41 | The default is 10 and the amount will change the amount in pixels that the view can move in any direction.
42 |
43 | **If you want to get a bit more creative, you can set the amount for each direction like this:**
44 |
45 | ```swift
46 | .parallax(minHorizontal: -20, maxHorizontal: 20, minVertical: -5, maxVertical: 5)
47 | ```
48 |
49 | This code above will make it so the view can move 20 pixels left and right and 5 up and down.
50 |
51 | **Complete examples:**
52 |
53 | ```swift
54 | .parallax(minHorizontal: 20, maxHorizontal: -20, minVertical: 5, maxVertical: -5, direction: .both)
55 | ```
56 |
57 | ```swift
58 | .parallax(amount: 22, direction: .horizontal)
59 | ```
60 |
61 | ### *Pro tip:* If you want to invert the direction that the effect occurs in set the min values to a positive and the max values to a negative.
62 |
63 | ## How to best make it work
64 |
65 | The Parallax effect uses `UIInterpolatingMotionEffect` and some representables and hosting controllers to get it working on SwiftUI views. UIKit requires that we specify the frame of the view so therefore I am using `GeometryReader` to get the proposed size that the parent suggested for your view.
66 |
67 | Therefore, because of the nature of `GeometryReader`, your view will now take up all available space.
68 |
69 | To get the effect to best work you should set your modifiers that will change how the view looks first, then the parallax, then modifiers that will change the layout of your view, such as `.frame()`
70 |
71 | Like this:
72 |
73 | 
74 |
75 | ## Real life example
76 |
77 | Here is a cool button I made:
78 |
79 | 
80 |
81 | ```swift
82 | import SwiftUI
83 | import ParallaxSwiftUI
84 |
85 | struct ContentView: View {
86 | var body: some View {
87 | Button(action: {
88 | print("Parallax is pretty cool!")
89 | }, label: {
90 | ZStack {
91 |
92 | /// Shadow
93 | LinearGradient(
94 | gradient: Gradient(colors: [.init(red: 0, green: 0.5, blue: 1), .purple]),
95 | startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 0)
96 | )
97 | .mask(
98 | RoundedRectangle(cornerRadius: 15, style: .continuous)
99 | .blur(radius: 18)
100 | )
101 | .blur(radius: 18)
102 | .opacity(0.8)
103 | .parallax(amount: 18)
104 | .frame(height: 60)
105 |
106 | /// Rectangle
107 | LinearGradient(
108 | gradient: Gradient(colors: [.init(red: 0, green: 0.5, blue: 1), .purple]),
109 | startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 0)
110 | )
111 | .mask(
112 | RoundedRectangle(cornerRadius: 15, style: .continuous)
113 | )
114 | .frame(height: 60)
115 |
116 | /// Text
117 | Text("Parallax")
118 | .font(.title3)
119 | .fontWeight(.bold)
120 | .foregroundColor(.primary)
121 | .shadow(radius: 5)
122 | .parallax(minHorizontal: 10, maxHorizontal: -10, minVertical: 10, maxVertical: -10, direction: .both)
123 | }
124 | })
125 | .padding()
126 | }
127 | }
128 | ```
129 |
--------------------------------------------------------------------------------
/Sources/ParallaxSwiftUI/ParallaxSwiftUI.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 | public func parallax(amount: CGFloat = 10, direction: ParallaxDirection = .both) -> some View {
5 | ParallaxView(
6 | view: AnyView(self),
7 | amount: amount,
8 | direction: direction
9 | )
10 | }
11 | public func parallax(minHorizontal: CGFloat = -10, maxHorizontal: CGFloat = 10, minVertical: CGFloat = -10, maxVertical: CGFloat = 10, direction: ParallaxDirection = .both) -> some View {
12 | ParallaxView(
13 | view: AnyView(self),
14 | minHorizontal: minHorizontal,
15 | maxHorizontal: maxHorizontal,
16 | minVertical: minVertical,
17 | maxVertical: maxVertical,
18 | direction: direction
19 | )
20 | }
21 | }
22 |
23 | public enum ParallaxDirection {
24 | case vertical
25 | case horizontal
26 | case both
27 | }
28 |
29 | /// A wrapper view to add a parallax effect to a SwiftUI view.
30 | struct ParallaxView: View {
31 |
32 | /// The view to apply the parallax too.
33 | let view: AnyView
34 |
35 | /// The amount of the parallax effect to be applied.
36 | let amount: CGFloat?
37 |
38 | let minHorizontal: CGFloat
39 | let maxHorizontal: CGFloat
40 |
41 | let minVertical: CGFloat
42 | let maxVertical: CGFloat
43 |
44 | let direction: ParallaxDirection
45 |
46 | init(view: AnyView, minHorizontal: CGFloat = -10, maxHorizontal: CGFloat = 10, minVertical: CGFloat = -10, maxVertical: CGFloat = 10, amount: CGFloat? = nil, direction: ParallaxDirection) {
47 | self.view = view
48 | self.direction = direction
49 |
50 | if amount == nil {
51 | self.amount = nil
52 | self.minHorizontal = minHorizontal
53 | self.maxHorizontal = maxHorizontal
54 | self.minVertical = minVertical
55 | self.maxVertical = maxVertical
56 | } else {
57 | self.amount = amount
58 | self.minHorizontal = -amount!
59 | self.maxHorizontal = amount!
60 | self.minVertical = -amount!
61 | self.maxVertical = amount!
62 | }
63 | }
64 |
65 | var body: some View {
66 | /// Using geometry reader we can get the proposed width and height of the view normally. Then we can pass that to the view controller.
67 | GeometryReader { geometry in
68 | ParallaxRepresentable(view: view, width: geometry.frame(in: .local).size.width, height: geometry.frame(in: .local).size.height, minHorizontal: minHorizontal, maxHorizontal: maxHorizontal, minVertical: minVertical, maxVertical: maxVertical, direction: direction)
69 | }
70 | }
71 | }
72 |
73 | /// Converts SwiftUI view to UIKit controller.
74 | struct ParallaxRepresentable: UIViewControllerRepresentable {
75 |
76 | let view: AnyView
77 | let width: CGFloat
78 | let height: CGFloat
79 |
80 | let minHorizontal: CGFloat
81 | let maxHorizontal: CGFloat
82 |
83 | let minVertical: CGFloat
84 | let maxVertical: CGFloat
85 |
86 | let direction: ParallaxDirection
87 |
88 | func makeUIViewController(context: Context) -> ParallaxController {
89 |
90 | let controller = ParallaxController()
91 |
92 | let hostingController = UIHostingController(rootView: view)
93 |
94 | controller.viewWidth = width
95 | controller.viewHeight = height
96 |
97 | controller.minHorizontal = minHorizontal
98 | controller.maxHorizontal = maxHorizontal
99 | controller.minVertical = minVertical
100 | controller.maxVertical = maxVertical
101 |
102 | controller.direction = direction
103 |
104 | controller.viewToChange = hostingController
105 |
106 | return controller
107 | }
108 |
109 | func updateUIViewController(_ uiViewController: ParallaxController, context: Context) {
110 | uiViewController.viewWidth = width
111 | uiViewController.viewHeight = height
112 | uiViewController.updateView()
113 | }
114 | }
115 |
116 | /// View controller that adds parallax effect to view of another view controller.
117 | class ParallaxController: UIViewController {
118 |
119 | var viewToChange: UIViewController?
120 | var viewWidth: CGFloat = 0
121 | var viewHeight: CGFloat = 0
122 |
123 | var minHorizontal: CGFloat = -10
124 | var maxHorizontal: CGFloat = 10
125 |
126 | var minVertical: CGFloat = -10
127 | var maxVertical: CGFloat = 10
128 |
129 | var direction: ParallaxDirection = .both
130 |
131 | override func viewDidLoad() {
132 | super.viewDidLoad()
133 |
134 | // Add SwiftUI view to view controller
135 | addChild(viewToChange!)
136 | view.addSubview(viewToChange!.view)
137 | viewToChange!.didMove(toParent: self)
138 |
139 | // Set frame
140 | viewToChange!.view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight)
141 | view.frame = viewToChange!.view.frame
142 |
143 | // Clear background colour
144 | viewToChange!.view.backgroundColor = .clear
145 | }
146 |
147 | func updateView() {
148 | viewToChange!.view.frame = CGRect(x: 0, y: 0, width: viewWidth, height: viewHeight)
149 | view.frame = viewToChange!.view.frame
150 | }
151 |
152 | let horizontal = UIInterpolatingMotionEffect(keyPath: "center.x", type: .tiltAlongHorizontalAxis)
153 | let vertical = UIInterpolatingMotionEffect(keyPath: "center.y", type: .tiltAlongVerticalAxis)
154 | let group = UIMotionEffectGroup()
155 |
156 | override func viewWillLayoutSubviews() {
157 |
158 | horizontal.minimumRelativeValue = minHorizontal
159 | horizontal.maximumRelativeValue = maxHorizontal
160 |
161 | vertical.minimumRelativeValue = minVertical
162 | vertical.maximumRelativeValue = maxVertical
163 |
164 | if direction == .horizontal {
165 | group.motionEffects = [horizontal]
166 | } else if direction == .vertical {
167 | group.motionEffects = [vertical]
168 | } else {
169 | group.motionEffects = [horizontal, vertical]
170 | }
171 |
172 | viewToChange!.view.addMotionEffect(group)
173 | }
174 |
175 | }
176 |
177 |
--------------------------------------------------------------------------------
/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Priva28/ParallaxSwiftUI/be5d0ba6c8fa69192e4675788aa13698b927cfb3/preview.gif
--------------------------------------------------------------------------------
/tutorial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Priva28/ParallaxSwiftUI/be5d0ba6c8fa69192e4675788aa13698b927cfb3/tutorial.png
--------------------------------------------------------------------------------