Swift 自定义动画中 CADisplayLink 的正确处理/清理/等?
Correct handling / cleanup / etc of CADisplayLink in Swift custom animation?
使用 CADisplayLink
、
考虑这个简单的同步动画
var link:CADisplayLink?
var startTime:Double = 0.0
let animTime:Double = 0.2
let animMaxVal:CGFloat = 0.4
private func yourAnim()
{
if ( link != nil )
{
link!.paused = true
//A:
link!.removeFromRunLoop(
NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
link = nil
}
link = CADisplayLink(target: self, selector: #selector(doorStep) )
startTime = CACurrentMediaTime()
link!.addToRunLoop(
NSRunLoop.currentRunLoop(), forMode:NSDefaultRunLoopMode)
}
func doorStep()
{
let elapsed = CACurrentMediaTime() - startTime
var ping = elapsed
if (elapsed > (animTime / 2.0)) {ping = animTime - elapsed}
let frac = ping / (animTime / 2.0)
yourAnimFunction(CGFloat(frac) * animMaxVal)
if (elapsed > animTime)
{
//B:
link!.paused = true
link!.removeFromRunLoop(
NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
link = nil
yourAnimFunction(0.0)
}
}
func killAnimation()
{
// for example if the cell disappears or is reused
//C:
????!!!!
}
好像有各种各样的问题。
在 (A:) 处,即使 link
不为空,也可能无法将其从 运行 循环中删除。 (例如,有人可能已经用 link = link:CADisplayLink()
初始化了它 - 尝试一下会崩溃。)
其次在 (B:) 似乎是一团糟......肯定有更好的(更多 Swift)方式,如果时间刚刚过期它为零怎么办?
终于在 (C:) 中,如果你想打破动画...我很沮丧,不知道什么是最好的。
实际上 A: 和 B: 处的代码应该是相同的调用权,一种清理调用。
这是一个简单的例子,展示了我将如何实现 CADisplayLink
(在 Swift 5 中):
class C { /// your view class or whatever
private var displayLink: CADisplayLink?
private var startTime = 0.0
private let animationLength = 5.0
func startDisplayLink() {
stopDisplayLink() /// make sure to stop a previous running display link
startTime = CACurrentMediaTime() // reset start time
/// create displayLink and add it to the run-loop
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidFire))
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
}
@objc func displayLinkDidFire(_ displayLink: CADisplayLink) {
var elapsedTime = CACurrentMediaTime() - startTime
if elapsedTime > animationLength {
stopDisplayLink()
elapsedTime = animationLength /// clamp the elapsed time to the animation length
}
/// do your animation logic here
}
/// invalidate display link if it's non-nil, then set to nil
func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
}
}
注意事项:
- 我们在这里使用
nil
来表示显示 link 不是 运行ning 的状态——因为没有简单的方法从无效的显示 link.
- 我们不使用
removeFromRunLoop()
,而是使用 invalidate()
,如果显示 link 尚未添加到 运行-,它不会崩溃环形。然而,这种情况一开始就不应该出现——因为我们总是在创建显示 link 后立即将其添加到 运行 循环中。
- 我们已将
displayLink
设为私有,以防止外部 类 将其置于意外状态(例如使其无效但不将其设置为 nil
)。
- 我们有一个
stopDisplayLink()
方法,它既使显示 link(如果非零)无效,又将其设置为 nil
——而不是复制和粘贴此逻辑.
- 我们不会在使显示 link 无效之前将
paused
设置为 true
,因为这是多余的。
- 我们不是在检查非零值后强制展开
displayLink
,而是使用可选链接,例如 displayLink?.invalidate()
(如果显示 link 不是零)。虽然强制展开在您给定的情况下可能是“安全的”(因为您正在检查 nil)——但在未来重构时它可能是不安全的,因为您可能会在不考虑这对强制展开有什么影响的情况下重新构建您的逻辑.
- 我们将
elapsed
时间限制在动画持续时间内,以确保后面的动画逻辑不会产生超出预期范围的值。
- 我们的更新方法
displayLinkDidFire(_:)
根据需要采用 CADisplayLink
类型的单个参数 by the documentation。
我知道这个问题已经有了很好的答案,但这里有另一种略有不同的方法,可以帮助实现独立于显示 link 帧速率的流畅动画。
**(Link 到此答案底部可用的演示项目 - 更新:演示项目源代码现已更新为 Swift 4)
对于我的实现,我选择将显示 link 包装在它自己的 class 中并设置一个委托引用,该引用将使用增量时间(上次显示之间的时间 link 调用和当前调用)这样我们就可以更流畅地执行我们的动画。
我目前正在使用此方法在游戏中同时为屏幕周围的 ~60 个视图设置动画。
首先,我们将定义我们的包装器将调用以通知更新事件的委托协议。
// defines an interface for receiving display update notifications
protocol DisplayUpdateReceiver: class {
func displayWillUpdate(deltaTime: CFTimeInterval)
}
接下来我们将定义我们的显示 link 包装器 class。此 class 将在初始化时采用委托引用。初始化后它会自动启动我们的显示 link,并在 deinit 时清理它。
import UIKit
class DisplayUpdateNotifier {
// **********************************************
// MARK: Variables
// **********************************************
/// A weak reference to the delegate/listener that will be notified/called on display updates
weak var listener: DisplayUpdateReceiver?
/// The display link that will be initiating our updates
internal var displayLink: CADisplayLink? = nil
/// Tracks the timestamp from the previous displayLink call
internal var lastTime: CFTimeInterval = 0.0
// **********************************************
// MARK: Setup & Tear Down
// **********************************************
deinit {
stopDisplayLink()
}
init(listener: DisplayUpdateReceiver) {
// setup our delegate listener reference
self.listener = listener
// setup & kick off the display link
startDisplayLink()
}
// **********************************************
// MARK: CADisplay Link
// **********************************************
/// Creates a new display link if one is not already running
private func startDisplayLink() {
guard displayLink == nil else {
return
}
displayLink = CADisplayLink(target: self, selector: #selector(linkUpdate))
displayLink?.add(to: .main, forMode: .commonModes)
lastTime = 0.0
}
/// Invalidates and destroys the current display link. Resets timestamp var to zero
private func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
lastTime = 0.0
}
/// Notifier function called by display link. Calculates the delta time and passes it in the delegate call.
@objc private func linkUpdate() {
// bail if our display link is no longer valid
guard let displayLink = displayLink else {
return
}
// get the current time
let currentTime = displayLink.timestamp
// calculate delta (
let delta: CFTimeInterval = currentTime - lastTime
// store as previous
lastTime = currentTime
// call delegate
listener?.displayWillUpdate(deltaTime: delta)
}
}
要使用它,您只需初始化包装器的实例,传入委托侦听器引用,然后根据增量时间更新动画。在此示例中,委托将更新调用传递给可动画视图(这样您可以跟踪多个动画视图并让每个视图通过此调用更新它们的位置)。
class ViewController: UIViewController, DisplayUpdateReceiver {
var displayLinker: DisplayUpdateNotifier?
var animView: MoveableView?
override func viewDidLoad() {
super.viewDidLoad()
// setup our animatable view and add as subview
animView = MoveableView.init(frame: CGRect.init(x: 150.0, y: 400.0, width: 20.0, height: 20.0))
animView?.configureMovement()
animView?.backgroundColor = .blue
view.addSubview(animView!)
// setup our display link notifier wrapper class
displayLinker = DisplayUpdateNotifier.init(listener: self)
}
// implement DisplayUpdateReceiver function to receive updates from display link wrapper class
func displayWillUpdate(deltaTime: CFTimeInterval) {
// pass the update call off to our animating view or views
_ = animView?.update(deltaTime: deltaTime)
// in this example, the animatable view will remove itself from its superview when its animation is complete and set a flag
// that it's ready to be used. We simply check if it's ready to be recycled, if so we reset its position and add it to
// our view again
if animView?.isReadyForReuse == true {
animView?.reset(center: CGPoint.init(x: CGFloat.random(low: 20.0, high: 300.0), y: CGFloat.random(low: 20.0, high: 700.0)))
view.addSubview(animView!)
}
}
}
我们的可移动视图更新函数如下所示:
func update(deltaTime: CFTimeInterval) -> Bool {
guard canAnimate == true, isReadyForReuse == false else {
return false
}
// by multiplying our x/y values by the delta time new values are generated that will generate a smooth animation independent of the framerate.
let smoothVel = CGPoint(x: CGFloat(Double(velocity.x)*deltaTime), y: CGFloat(Double(velocity.y)*deltaTime))
let smoothAccel = CGPoint(x: CGFloat(Double(acceleration.x)*deltaTime), y: CGFloat(Double(acceleration.y)*deltaTime))
// update velocity with smoothed acceleration
velocity.adding(point: smoothAccel)
// update center with smoothed velocity
center.adding(point: smoothVel)
currentTime += 0.01
if currentTime >= timeLimit {
canAnimate = false
endAnimation()
return false
}
return true
}
如果您想查看完整的演示项目,可以从 GitHub 此处下载:CADisplayLink Demo Project
以上是如何高效使用CADisplayLink的最佳范例。感谢@Fattie 和@digitalHound
我无法抗拒在使用 WKWebView 的 PdfViewer 中通过 'digitalHound' 添加我对 CADisplayLink 和 DisplayUpdater 类 的使用。
我的要求是继续以用户可选择的速度自动滚动 pdf。
可能这里的答案不对,但我想在这里展示一下CADisplayLink的用法。 (对于像我这样的人,谁可以实现他们的要求。)
//
// PdfViewController.swift
//
import UIKit
import WebKit
class PdfViewController: UIViewController, DisplayUpdateReceiver {
@IBOutlet var mySpeedScrollSlider: UISlider! // UISlider in storyboard
var displayLinker: DisplayUpdateNotifier?
var myPdfFileName = ""
var myPdfFolderPath = ""
var myViewTitle = "Pdf View"
var myCanAnimate = false
var mySlowSkip = 0.0
// 0.125<=slow, 0.25=normal, 0.5=fast, 0.75>=faster
var cuScrollSpeed = 0.25
fileprivate var myPdfWKWebView = WKWebView(frame: CGRect.zero)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.title = myViewTitle
let leftItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(PdfViewController.PdfBackClick))
navigationItem.leftBarButtonItem = leftItem
self.view.backgroundColor = UIColor.white.cgColor
mySpeedScrollSlider.minimumValue = 0.05
mySpeedScrollSlider.maximumValue = 4.0
mySpeedScrollSlider.isContinuous = true
mySpeedScrollSlider.addTarget(self, action: #selector(PdfViewController.updateSlider), for: [.valueChanged])
mySpeedScrollSlider.setValue(Float(cuScrollSpeed), animated: false)
mySpeedScrollSlider.backgroundColor = UIColor.white.cgColor
self.configureWebView()
let folderUrl = URL(fileURLWithPath: myPdfFolderPath)
let url = URL(fileURLWithPath: myPdfFolderPath + myPdfFileName)
myPdfWKWebView.loadFileURL(url, allowingReadAccessTo: folderUrl)
}
//MARK: - Button Action
@objc func PdfBackClick()
{
_ = self.navigationController?.popViewController(animated: true)
}
@objc func updateSlider()
{
if ( mySpeedScrollSlider.value <= mySpeedScrollSlider.minimumValue ) {
myCanAnimate = false
} else {
myCanAnimate = true
}
cuScrollSpeed = Double(mySpeedScrollSlider.value)
}
fileprivate func configureWebView() {
myPdfWKWebView.frame = view.bounds
myPdfWKWebView.translatesAutoresizingMaskIntoConstraints = false
myPdfWKWebView.navigationDelegate = self
myPdfWKWebView.isMultipleTouchEnabled = true
myPdfWKWebView.scrollView.alwaysBounceVertical = true
myPdfWKWebView.layer.backgroundColor = UIColor.red.cgColor //test
view.addSubview(myPdfWKWebView)
myPdfWKWebView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor ).isActive = true
myPdfWKWebView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
myPdfWKWebView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
myPdfWKWebView.bottomAnchor.constraint(equalTo: mySpeedScrollSlider.topAnchor).isActive = true
}
//MARK: - DisplayUpdateReceiver delegate
func displayWillUpdate(deltaTime: CFTimeInterval) {
guard myCanAnimate == true else {
return
}
var maxSpeed = 0.0
if cuScrollSpeed < 0.5 {
if mySlowSkip > 0.25 {
mySlowSkip = 0.0
} else {
mySlowSkip += cuScrollSpeed
return
}
maxSpeed = 0.5
} else {
maxSpeed = cuScrollSpeed
}
let scrollViewHeight = self.myPdfWKWebView.scrollView.frame.size.height
let scrollContentSizeHeight = self.myPdfWKWebView.scrollView.contentSize.height
let scrollOffset = self.myPdfWKWebView.scrollView.contentOffset.y
let xOffset = self.myPdfWKWebView.scrollView.contentOffset.x
if (scrollOffset + scrollViewHeight >= scrollContentSizeHeight)
{
return
}
let newYOffset = CGFloat( max( min( deltaTime , 1 ), maxSpeed ) )
self.myPdfWKWebView.scrollView.setContentOffset(CGPoint(x: xOffset, y: scrollOffset+newYOffset), animated: false)
}
}
extension PdfViewController: WKNavigationDelegate {
// MARK: - WKNavigationDelegate
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
//print("didStartProvisionalNavigation")
}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
//print("didFinish")
displayLinker = DisplayUpdateNotifier.init(listener: self)
myCanAnimate = true
}
public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
//print("didFailProvisionalNavigation error:\(error)")
}
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
//print("didFail")
}
}
从另一个视图调用示例如下。
从文档文件夹加载 PDF 文件。
func callPdfViewController( theFileName:String, theFileParentPath:String){
if ( !theFileName.isEmpty && !theFileParentPath.isEmpty ) {
let pdfViewController = self.storyboard!.instantiateViewController(withIdentifier: "PdfViewController") as? PdfViewController
pdfViewController?.myPdfFileName = theFileName
pdfViewController?.myPdfFolderPath = theFileParentPath
self.navigationController!.pushViewController(pdfViewController!, animated: true)
} else {
// Show error.
}
}
此示例可能是 'modified' 以加载网页并以用户选择的速度自动滚动它们。
此致
桑杰。
使用 CADisplayLink
、
var link:CADisplayLink?
var startTime:Double = 0.0
let animTime:Double = 0.2
let animMaxVal:CGFloat = 0.4
private func yourAnim()
{
if ( link != nil )
{
link!.paused = true
//A:
link!.removeFromRunLoop(
NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
link = nil
}
link = CADisplayLink(target: self, selector: #selector(doorStep) )
startTime = CACurrentMediaTime()
link!.addToRunLoop(
NSRunLoop.currentRunLoop(), forMode:NSDefaultRunLoopMode)
}
func doorStep()
{
let elapsed = CACurrentMediaTime() - startTime
var ping = elapsed
if (elapsed > (animTime / 2.0)) {ping = animTime - elapsed}
let frac = ping / (animTime / 2.0)
yourAnimFunction(CGFloat(frac) * animMaxVal)
if (elapsed > animTime)
{
//B:
link!.paused = true
link!.removeFromRunLoop(
NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
link = nil
yourAnimFunction(0.0)
}
}
func killAnimation()
{
// for example if the cell disappears or is reused
//C:
????!!!!
}
好像有各种各样的问题。
在 (A:) 处,即使 link
不为空,也可能无法将其从 运行 循环中删除。 (例如,有人可能已经用 link = link:CADisplayLink()
初始化了它 - 尝试一下会崩溃。)
其次在 (B:) 似乎是一团糟......肯定有更好的(更多 Swift)方式,如果时间刚刚过期它为零怎么办?
终于在 (C:) 中,如果你想打破动画...我很沮丧,不知道什么是最好的。
实际上 A: 和 B: 处的代码应该是相同的调用权,一种清理调用。
这是一个简单的例子,展示了我将如何实现 CADisplayLink
(在 Swift 5 中):
class C { /// your view class or whatever
private var displayLink: CADisplayLink?
private var startTime = 0.0
private let animationLength = 5.0
func startDisplayLink() {
stopDisplayLink() /// make sure to stop a previous running display link
startTime = CACurrentMediaTime() // reset start time
/// create displayLink and add it to the run-loop
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidFire))
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
}
@objc func displayLinkDidFire(_ displayLink: CADisplayLink) {
var elapsedTime = CACurrentMediaTime() - startTime
if elapsedTime > animationLength {
stopDisplayLink()
elapsedTime = animationLength /// clamp the elapsed time to the animation length
}
/// do your animation logic here
}
/// invalidate display link if it's non-nil, then set to nil
func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
}
}
注意事项:
- 我们在这里使用
nil
来表示显示 link 不是 运行ning 的状态——因为没有简单的方法从无效的显示 link. - 我们不使用
removeFromRunLoop()
,而是使用invalidate()
,如果显示 link 尚未添加到 运行-,它不会崩溃环形。然而,这种情况一开始就不应该出现——因为我们总是在创建显示 link 后立即将其添加到 运行 循环中。 - 我们已将
displayLink
设为私有,以防止外部 类 将其置于意外状态(例如使其无效但不将其设置为nil
)。 - 我们有一个
stopDisplayLink()
方法,它既使显示 link(如果非零)无效,又将其设置为nil
——而不是复制和粘贴此逻辑. - 我们不会在使显示 link 无效之前将
paused
设置为true
,因为这是多余的。 - 我们不是在检查非零值后强制展开
displayLink
,而是使用可选链接,例如displayLink?.invalidate()
(如果显示 link 不是零)。虽然强制展开在您给定的情况下可能是“安全的”(因为您正在检查 nil)——但在未来重构时它可能是不安全的,因为您可能会在不考虑这对强制展开有什么影响的情况下重新构建您的逻辑. - 我们将
elapsed
时间限制在动画持续时间内,以确保后面的动画逻辑不会产生超出预期范围的值。 - 我们的更新方法
displayLinkDidFire(_:)
根据需要采用CADisplayLink
类型的单个参数 by the documentation。
我知道这个问题已经有了很好的答案,但这里有另一种略有不同的方法,可以帮助实现独立于显示 link 帧速率的流畅动画。
**(Link 到此答案底部可用的演示项目 - 更新:演示项目源代码现已更新为 Swift 4)
对于我的实现,我选择将显示 link 包装在它自己的 class 中并设置一个委托引用,该引用将使用增量时间(上次显示之间的时间 link 调用和当前调用)这样我们就可以更流畅地执行我们的动画。
我目前正在使用此方法在游戏中同时为屏幕周围的 ~60 个视图设置动画。
首先,我们将定义我们的包装器将调用以通知更新事件的委托协议。
// defines an interface for receiving display update notifications
protocol DisplayUpdateReceiver: class {
func displayWillUpdate(deltaTime: CFTimeInterval)
}
接下来我们将定义我们的显示 link 包装器 class。此 class 将在初始化时采用委托引用。初始化后它会自动启动我们的显示 link,并在 deinit 时清理它。
import UIKit
class DisplayUpdateNotifier {
// **********************************************
// MARK: Variables
// **********************************************
/// A weak reference to the delegate/listener that will be notified/called on display updates
weak var listener: DisplayUpdateReceiver?
/// The display link that will be initiating our updates
internal var displayLink: CADisplayLink? = nil
/// Tracks the timestamp from the previous displayLink call
internal var lastTime: CFTimeInterval = 0.0
// **********************************************
// MARK: Setup & Tear Down
// **********************************************
deinit {
stopDisplayLink()
}
init(listener: DisplayUpdateReceiver) {
// setup our delegate listener reference
self.listener = listener
// setup & kick off the display link
startDisplayLink()
}
// **********************************************
// MARK: CADisplay Link
// **********************************************
/// Creates a new display link if one is not already running
private func startDisplayLink() {
guard displayLink == nil else {
return
}
displayLink = CADisplayLink(target: self, selector: #selector(linkUpdate))
displayLink?.add(to: .main, forMode: .commonModes)
lastTime = 0.0
}
/// Invalidates and destroys the current display link. Resets timestamp var to zero
private func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
lastTime = 0.0
}
/// Notifier function called by display link. Calculates the delta time and passes it in the delegate call.
@objc private func linkUpdate() {
// bail if our display link is no longer valid
guard let displayLink = displayLink else {
return
}
// get the current time
let currentTime = displayLink.timestamp
// calculate delta (
let delta: CFTimeInterval = currentTime - lastTime
// store as previous
lastTime = currentTime
// call delegate
listener?.displayWillUpdate(deltaTime: delta)
}
}
要使用它,您只需初始化包装器的实例,传入委托侦听器引用,然后根据增量时间更新动画。在此示例中,委托将更新调用传递给可动画视图(这样您可以跟踪多个动画视图并让每个视图通过此调用更新它们的位置)。
class ViewController: UIViewController, DisplayUpdateReceiver {
var displayLinker: DisplayUpdateNotifier?
var animView: MoveableView?
override func viewDidLoad() {
super.viewDidLoad()
// setup our animatable view and add as subview
animView = MoveableView.init(frame: CGRect.init(x: 150.0, y: 400.0, width: 20.0, height: 20.0))
animView?.configureMovement()
animView?.backgroundColor = .blue
view.addSubview(animView!)
// setup our display link notifier wrapper class
displayLinker = DisplayUpdateNotifier.init(listener: self)
}
// implement DisplayUpdateReceiver function to receive updates from display link wrapper class
func displayWillUpdate(deltaTime: CFTimeInterval) {
// pass the update call off to our animating view or views
_ = animView?.update(deltaTime: deltaTime)
// in this example, the animatable view will remove itself from its superview when its animation is complete and set a flag
// that it's ready to be used. We simply check if it's ready to be recycled, if so we reset its position and add it to
// our view again
if animView?.isReadyForReuse == true {
animView?.reset(center: CGPoint.init(x: CGFloat.random(low: 20.0, high: 300.0), y: CGFloat.random(low: 20.0, high: 700.0)))
view.addSubview(animView!)
}
}
}
我们的可移动视图更新函数如下所示:
func update(deltaTime: CFTimeInterval) -> Bool {
guard canAnimate == true, isReadyForReuse == false else {
return false
}
// by multiplying our x/y values by the delta time new values are generated that will generate a smooth animation independent of the framerate.
let smoothVel = CGPoint(x: CGFloat(Double(velocity.x)*deltaTime), y: CGFloat(Double(velocity.y)*deltaTime))
let smoothAccel = CGPoint(x: CGFloat(Double(acceleration.x)*deltaTime), y: CGFloat(Double(acceleration.y)*deltaTime))
// update velocity with smoothed acceleration
velocity.adding(point: smoothAccel)
// update center with smoothed velocity
center.adding(point: smoothVel)
currentTime += 0.01
if currentTime >= timeLimit {
canAnimate = false
endAnimation()
return false
}
return true
}
如果您想查看完整的演示项目,可以从 GitHub 此处下载:CADisplayLink Demo Project
以上是如何高效使用CADisplayLink的最佳范例。感谢@Fattie 和@digitalHound
我无法抗拒在使用 WKWebView 的 PdfViewer 中通过 'digitalHound' 添加我对 CADisplayLink 和 DisplayUpdater 类 的使用。 我的要求是继续以用户可选择的速度自动滚动 pdf。
可能这里的答案不对,但我想在这里展示一下CADisplayLink的用法。 (对于像我这样的人,谁可以实现他们的要求。)
//
// PdfViewController.swift
//
import UIKit
import WebKit
class PdfViewController: UIViewController, DisplayUpdateReceiver {
@IBOutlet var mySpeedScrollSlider: UISlider! // UISlider in storyboard
var displayLinker: DisplayUpdateNotifier?
var myPdfFileName = ""
var myPdfFolderPath = ""
var myViewTitle = "Pdf View"
var myCanAnimate = false
var mySlowSkip = 0.0
// 0.125<=slow, 0.25=normal, 0.5=fast, 0.75>=faster
var cuScrollSpeed = 0.25
fileprivate var myPdfWKWebView = WKWebView(frame: CGRect.zero)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.title = myViewTitle
let leftItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(PdfViewController.PdfBackClick))
navigationItem.leftBarButtonItem = leftItem
self.view.backgroundColor = UIColor.white.cgColor
mySpeedScrollSlider.minimumValue = 0.05
mySpeedScrollSlider.maximumValue = 4.0
mySpeedScrollSlider.isContinuous = true
mySpeedScrollSlider.addTarget(self, action: #selector(PdfViewController.updateSlider), for: [.valueChanged])
mySpeedScrollSlider.setValue(Float(cuScrollSpeed), animated: false)
mySpeedScrollSlider.backgroundColor = UIColor.white.cgColor
self.configureWebView()
let folderUrl = URL(fileURLWithPath: myPdfFolderPath)
let url = URL(fileURLWithPath: myPdfFolderPath + myPdfFileName)
myPdfWKWebView.loadFileURL(url, allowingReadAccessTo: folderUrl)
}
//MARK: - Button Action
@objc func PdfBackClick()
{
_ = self.navigationController?.popViewController(animated: true)
}
@objc func updateSlider()
{
if ( mySpeedScrollSlider.value <= mySpeedScrollSlider.minimumValue ) {
myCanAnimate = false
} else {
myCanAnimate = true
}
cuScrollSpeed = Double(mySpeedScrollSlider.value)
}
fileprivate func configureWebView() {
myPdfWKWebView.frame = view.bounds
myPdfWKWebView.translatesAutoresizingMaskIntoConstraints = false
myPdfWKWebView.navigationDelegate = self
myPdfWKWebView.isMultipleTouchEnabled = true
myPdfWKWebView.scrollView.alwaysBounceVertical = true
myPdfWKWebView.layer.backgroundColor = UIColor.red.cgColor //test
view.addSubview(myPdfWKWebView)
myPdfWKWebView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor ).isActive = true
myPdfWKWebView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
myPdfWKWebView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
myPdfWKWebView.bottomAnchor.constraint(equalTo: mySpeedScrollSlider.topAnchor).isActive = true
}
//MARK: - DisplayUpdateReceiver delegate
func displayWillUpdate(deltaTime: CFTimeInterval) {
guard myCanAnimate == true else {
return
}
var maxSpeed = 0.0
if cuScrollSpeed < 0.5 {
if mySlowSkip > 0.25 {
mySlowSkip = 0.0
} else {
mySlowSkip += cuScrollSpeed
return
}
maxSpeed = 0.5
} else {
maxSpeed = cuScrollSpeed
}
let scrollViewHeight = self.myPdfWKWebView.scrollView.frame.size.height
let scrollContentSizeHeight = self.myPdfWKWebView.scrollView.contentSize.height
let scrollOffset = self.myPdfWKWebView.scrollView.contentOffset.y
let xOffset = self.myPdfWKWebView.scrollView.contentOffset.x
if (scrollOffset + scrollViewHeight >= scrollContentSizeHeight)
{
return
}
let newYOffset = CGFloat( max( min( deltaTime , 1 ), maxSpeed ) )
self.myPdfWKWebView.scrollView.setContentOffset(CGPoint(x: xOffset, y: scrollOffset+newYOffset), animated: false)
}
}
extension PdfViewController: WKNavigationDelegate {
// MARK: - WKNavigationDelegate
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
//print("didStartProvisionalNavigation")
}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
//print("didFinish")
displayLinker = DisplayUpdateNotifier.init(listener: self)
myCanAnimate = true
}
public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
//print("didFailProvisionalNavigation error:\(error)")
}
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
//print("didFail")
}
}
从另一个视图调用示例如下。
从文档文件夹加载 PDF 文件。
func callPdfViewController( theFileName:String, theFileParentPath:String){
if ( !theFileName.isEmpty && !theFileParentPath.isEmpty ) {
let pdfViewController = self.storyboard!.instantiateViewController(withIdentifier: "PdfViewController") as? PdfViewController
pdfViewController?.myPdfFileName = theFileName
pdfViewController?.myPdfFolderPath = theFileParentPath
self.navigationController!.pushViewController(pdfViewController!, animated: true)
} else {
// Show error.
}
}
此示例可能是 'modified' 以加载网页并以用户选择的速度自动滚动它们。
此致
桑杰。