I'm exploring how to implement page curling in SwiftUI, similar to what's available in UIKit using UIPageViewController with the .pageCurl transition style.
let PagesCurl = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: nil)
In SwiftUI, I'm looking to achieve a page curl effect where users can flip pages with their finger, likely to the style seen in Apple Book Stores. Here's a simplified SwiftUI example I've been working on:
struct Episode1View: View {
@State private var currentPage = 0
let contentCount = 11 // Number of content slices
var body: some View {
TabView(selection: $currentPage) {
ForEach(1...contentCount, id: \.self) { index in
BeforeThePage(imageName: "C1 Slice \(index)")
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
.rotation3DEffect(
.degrees(currentPage == 0 ? 0 : 180),
axis: (x: 0, y: 1, z: 0),
anchor: .trailing,
perspective: 0.5
)
.animation(.default)
}
}
struct BeforeThePage: View {
let imageName: String
var body: some View {
Image(imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
}
}
If you have any tips or insights on achieving a realistic page curling effect in SwiftUI, I'd greatly appreciate your input. Thank you!
I have a page curl solution in SwiftUI, iOS and macOS.
Page curling on iPad:
Page curling is demanding but I managed to hide the complexity in a series of view modifiers.
I) Code used to display a curlable page in iOS:
The modifier pageCurlable takes 5 essential parameters, a trigger, the curling direction, the duration of the curl, the UIView that displays the current page and a refreshPage closure that displays the next page. In my context I need the document parameter because the document settings define which background the views have.
The .onChange modifier of the page ID decides whether the page curls is forward or backward and initiates the page curl by toggling the trigger.
The pageCurling modifier uses internally 2 other modifiers. One calculates a static page curl image and the second animates this static image.
How to get a CIImage from a SwiftUI view? In my understanding SwiftUI views are merely recipes how to get a view that can be displayed. For rendering they must be embedded in an UIHostingView. The currently displayed page is already embedded - I transmit the front view. For the future view (the next page) I use a specific PageCurlFutureView UIViewRepresentable.
Here is the extension on UIView to get a CIImage:
On macOS this is an extension on NSView:
For the forward page curl the source of the image is the currently displayed SwiftUI view.
II) View modifier to generate a static page curl image:
The PageCurl modifier has several static variables which carry the CIImages initialImage and futureImage. As soon as they are set the corresponding UIImages are calculated which will be used by SwiftUI.
It has three variables which define the state of the page curling: progress, angle and curlForward to define which image to take for calculating the page curl, the initialImage or the futureImage.
It has a central function which generates a curled image from the input images:
func pageCurlImage(progress:Float, angle:Float) -> UIImageand it has a static function to convert portions of an UIView to a CIImage:
@MainActor static func image(rect:CGRect?, view:UIView?) -> CIImage?III) Page curl animation
With the new view function
pageCurl(progress:Double, angle:Double, forward:Bool)I can convert a SwiftUI view into its statically curled version. What I need is an animated series of these views.I use the SwiftUI .keyframeAnimator modifier for this. KeyframeAnimators change parameters from a starting value to an ending value stepwise in a defined way. They are triggered.
To describe the parameters a simple struct is used:
This animation view modifier starts itself using the .onAppear modifier and performs cleanup after the end of the animation by resetting the PageCurl static images to nil and setting the parameter isPageCurling to false.
Remains the finally used PageCurlable modifier. It is a relatively complex conglomerate of ZStacks of the original SwiftUI view and SwiftUI Image views using the UIImages produced from the former modifier. It uses several state parameters to decide which ZStack to display under which circumstances. In this modifier the initialImage and futureImage are calculated when required.
IV) PageCurlable Modifier
This modifier use the PageCurlFutureView struct to grab a picture of the next page for curling when curling backwards:
The emptyImage extension on UIImage:
The emptyImage extension on NSImage: