├── .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 | ![tutorial](https://github.com/Priva28/ParallaxSwiftUI/blob/main/tutorial.png) 74 | 75 | ## Real life example 76 | 77 | Here is a cool button I made: 78 | 79 | ![preview](https://github.com/Priva28/ParallaxSwiftUI/blob/main/preview.gif) 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 --------------------------------------------------------------------------------