I am creating a PDFViewer application.
I have set the autoScale Property of the PDFViewer to true, so that it the view expands to the width of the screen.
Works fine with large PDF documents.
But when the document is a single page document, the page automatically scrolls down to the end of the page, instead of starting with the beginning.
I just cant understand the root cause of it.
What am I missing here?
This is what I did, it's a bit hacky
if let scrollView = pdfView.subviews.first as? UIScrollView {
scrollView.contentOffset.y = 0.0
}
I think this is a bug in PDFView.
As workaround I suggest to manually scroll top the top:
self.pdfView.document = pdfDocument;
NSPoint pt = NSMakePoint(0.0, [self.pdfView.documentView bounds].size.height);
[self.pdfView.documentView scrollPoint:pt];
Bug Reporter
rdar://37942090: PDFview scrolls to bottom of the page in single page document.
I propose this solution for scrolling to top of first page (it works on iOS 11.3):
if let document = pdfView.document,
let firstPage = document.page(at: 0)
{
let firstPageBounds = firstPage.bounds(for: pdfView.displayBox)
pdfView.go(to: CGRect(x: 0, y: firstPageBounds.height, width: 1.0, height: 1.0), on: firstPage)
}
Looks like the go command needs to be done on the next run through the run loop to work reliably:
DispatchQueue.main.async
{
guard let firstPage = pdfView.document?.page(at: 0) else { return }
pdfView.go(to: CGRect(x: 0, y: Int.max, width: 0, height: 0), on: firstPage)
}
Tested on iOS 12
Expanding on Petro's answer, I had some issues with rotated pages and cropped pages, but this seems to work universally. I actually tested it on rotated pages, along with an annotation in the corner to verify that I was selecting the right corner:
guard let firstPage = pdfView!.document?.page(at: 0) else {
return;
}
let firstPageBounds = firstPage.bounds(for: pdfView!.displayBox)
switch (firstPage.rotation % 360) {
case 0:
topLeftX = firstPageBounds.minX
topLeftY = firstPageBounds.maxY
case 90:
topLeftX = firstPageBounds.minX
topLeftY = firstPageBounds.minY
case 180:
topLeftX = firstPageBounds.maxX
topLeftY = firstPageBounds.minY
case 270:
topLeftX = firstPageBounds.maxX
topLeftY = firstPageBounds.maxY
default:
print ("Invalid rotation value, not divisible by 90")
}
pdfView!.go(to: CGRect(x: topLeftX, y: topLeftY, width: 1.0, height: 1.0), on: firstPage)
I was loading the document in viewDidLoad, then I moved it to viewDidAppear, and it worked for me.
The rotation of the PDFPage matters! This is the Objective-C version of Oded:
- (void)scrollToTopOfPage:(PDFPage *)page {
CGRect pageBounds = [page boundsForBox:self.displayBox];
CGFloat topLeftX = 0;
CGFloat topLeftY = 0;
switch (page.rotation % 360) {
case 0:
topLeftX = CGRectGetMinX(pageBounds);
topLeftY = CGRectGetMaxY(pageBounds);
break;
case 90:
topLeftX = CGRectGetMinX(pageBounds);
topLeftY = CGRectGetMinY(pageBounds);
break;
case 180:
topLeftX = CGRectGetMaxX(pageBounds);
topLeftY = CGRectGetMinY(pageBounds);
break;
case 270:
topLeftX = CGRectGetMaxX(pageBounds);
topLeftY = CGRectGetMaxY(pageBounds);
break;
default:
break;
}
[self goToRect:CGRectMake(topLeftX, topLeftY, 1, 1) onPage:page];
}
if let document = PDFDocument(url: viewModel.url) {
pdfView.document = document
// avoid iOS autoscale issue
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.pdfView.autoScales = true
}
}
I used the work around to define the autoScales property a bit after the loading of the document. The first page is nicely fit on top then.
For Objective-C use:
PDFPage *firstPage = [pdfView.document pageAtIndex:0];
CGRect firstPageBounds = [firstPage boundsForBox:pdfView.displayBox];
[pdfView goToRect:CGRectMake(0, firstPageBounds.size.height, 1, 1) onPage:firstPage];
Related
I am making an IOS App in which I am rotating an imageView. Now, All things are complete. But when the rotation is completed. x, y, of that image View is changing .. How to resolve it . I am attaching code and screenshots so that you can easily find my problem.
let i = defaults.integerForKey("myown")
let g = nViews[i].frame
imageViews[i].transform = CGAffineTransformRotate(imageViews[i].transform, recognizer.rotation)
// nViews[i].transform = CGAffineTransformRotate(imageViews[i].transform, recognizer.rotation)
//
recognizer.rotation = 0
if recognizer.state == UIGestureRecognizerState.Ended
{
let f = imageViews[i].frame
// print(f.size.width) -->111.576689146311
// print(f.size.height) -->111.576689146311
// nViews[i].frame = f
nButtons[i].frame = CGRect(x: f.origin.x - 10, y: f.origin.y - 10, width: 25, height: 25)
// imageView.addSubview(nViews[i])
imageView.addSubview(nButtons[i])
}
Now the another view which is on that imageView become rectangular because of that changes.. As shown in screenshot first time when imageView is not rotated the view is perfect but when it rotates view becomes rectangular.. Hope you will understand problem now
Try to use bounds instead of frame: let g = nViews[i].bounds and let f = imageViews[i].bounds. See When to Use Bound and When to use Frame
I have a "U" shaped UIBezierPath which I use as the path for my myImage.layer to animate on. I also have a scrollView. My goal is to have a custom "Pull to Refresh" animation.
The problem I am having is that I want my myImage.layer to update based on how much the scrollView scrolled.
As the scrollView is pulled down, the myImage.layer animates along a "U" shape path. This is the path in my code which I created as a UIBezierPath.
This is how I calculate how far the scrollView is pulled down:
func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = CGFloat(max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0))
self.progress = min(max(offsetY / frame.size.height, 0.0), 1.0)
if !isRefreshing {
redrawFromProgress(self.progress)
}
}
This is the function to dynamically update the position (it is not working):
func redrawFromProgress(progress: CGFloat) {
// PROBLEM: This is not correct. Only the `x` position is dynamic based on scrollView position.
// The `y` position is static.
// I want this to be dynamic based on how much the scrollView scrolled.
myImage.layer.position = CGPoint(x: progress, y: 50)
}
Basically, this is what I want:
If the scrollView scrolled is 0.0, then the myImage.layer position should be CGPoint(x: 0, y: 0) or the starting point of the path.
If the scrollView scrolled is 0.5 (50%), then the myImage.layer position should be at 50% of the path, I don't know what the CGPoint value would be here.
and so on...
I tried getting the CGPoint values along the UIBezierPath and based on the % of the scrollView scrolled, assign that CGPoint value to it but don't know how to do this. I also looked at this post but I can't get it to work for me.
EDIT QUESTION 1:
By using this extension, I was able to get an array of CGPoints which contain 10 values based on my UIBezierPath:
extension CGPath {
func forEachPoint(#noescape body: #convention(block) (CGPathElement) -> Void) {
typealias Body = #convention(block) (CGPathElement) -> Void
func callback(info: UnsafeMutablePointer<Void>, element: UnsafePointer<CGPathElement>) {
let body = unsafeBitCast(info, Body.self)
body(element.memory)
}
// print(sizeofValue(body))
let unsafeBody = unsafeBitCast(body, UnsafeMutablePointer<Void>.self)
CGPathApply(self, unsafeBody, callback)
}
func getPathElementsPoints() -> [CGPoint] {
var arrayPoints : [CGPoint]! = [CGPoint]()
self.forEachPoint { element in
switch (element.type) {
case CGPathElementType.MoveToPoint:
arrayPoints.append(element.points[0])
case .AddLineToPoint:
arrayPoints.append(element.points[0])
case .AddQuadCurveToPoint:
arrayPoints.append(element.points[0])
arrayPoints.append(element.points[1])
case .AddCurveToPoint:
arrayPoints.append(element.points[0])
arrayPoints.append(element.points[1])
arrayPoints.append(element.points[2])
default: break
}
}
return arrayPoints
}
I also rewrote the function above called redrawFromProgress(progress: CGFloat) to this:
func redrawFromProgress(progress: CGFloat) {
let enterPath = paths[0]
let pathPointsArray = enterPath.CGPath
let junctionPoints = pathPointsArray.getPathElementsPoints()
// print(junctionPoints.count) // There are 10 junctionPoints
// progress means how much the scrollView has been pulled down,
// it goes from 0.0 to 1.0.
if progress <= 0.1 {
myImage.layer.position = junctionPoints[0]
} else if progress > 0.1 && progress <= 0.2 {
myImage.layer.position = junctionPoints[1]
} else if progress > 0.2 && progress <= 0.3 {
myImage.layer.position = junctionPoints[2]
} else if progress > 0.3 && progress <= 0.4 {
myImage.layer.position = junctionPoints[3]
} else if progress > 0.4 && progress <= 0.5 {
myImage.layer.position = junctionPoints[4]
} else if progress > 0.5 && progress <= 0.6 {
myImage.layer.position = junctionPoints[5]
} else if progress > 0.6 && progress <= 0.7 {
myImage.layer.position = junctionPoints[6]
} else if progress > 0.7 && progress <= 0.8 {
myImage.layer.position = junctionPoints[7]
} else if progress > 0.8 && progress <= 0.9 {
myImage.layer.position = junctionPoints[8]
} else if progress > 0.9 && progress <= 1.0 {
myImage.layer.position = junctionPoints[9]
}
}
If I pull down the scrollView very slow, the myImage.layer actually follows the path. The only problem is that if I pull down on the scrollView very fast, then the myImage.layer jumps to the last point. Could it be because of the way I wrote the if statement above?
Any ideas?
Thanks to #Sam Falconer for making me aware of this:
Your code is relying on the scrollViewDidScroll delegate callback to be called frequently enough to hit all of your keyframe points. When you pull quickly on the scroll view, it does not call that method frequently enough, causing the jump.
Once I confirmed this, he also helped by mentioning:
Additionally, you will find the CAKeyframeAnimation class to be useful.
With CAKeyfraneAnimation I am able to manually control it's value with this code:
func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = CGFloat(max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0))
self.progress = min(max(offsetY / frame.size.height, 0.0), 1.0)
if !isRefreshing {
redrawFromProgress(self.progress)
}
}
func redrawFromProgress(progress: CGFloat) {
// Animate image along enter path
let pathAnimation = CAKeyframeAnimation(keyPath: "position")
pathAnimation.path = myPath.CGPath
pathAnimation.calculationMode = kCAAnimationPaced
pathAnimation.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)]
pathAnimation.beginTime = 1e-100
pathAnimation.duration = 1.0
pathAnimation.timeOffset = CFTimeInterval() + Double(progress)
pathAnimation.removedOnCompletion = false
pathAnimation.fillMode = kCAFillModeForwards
imageLayer.addAnimation(pathAnimation, forKey: nil)
imageLayer.position = enterPath.currentPoint
}
Thanks again for the help guys!
Your code is relying on the scrollViewDidScroll delegate callback to be called frequently enough to hit all of your keyframe points. When you pull quickly on the scroll view, it does not call that method frequently enough, causing the jump.
You may want to try calculating a custom path based on a segment of an arc representing the path between your current position, and your desired position. Basing an animation on this, instead of deconstructing your custom path (which looks very close to just being an arc), may be easier.
CGPathAddArc() with x, y, and r being constant, should get you 90% to what your path is now. You could also get fancier with the path to add that line segment like you have at the beginning of your path. It would just take a bit more work to get the partial path to come out right for all the "I'm at this position, get me a path to this other position" logic.
Additionally, you will find the CAKeyframeAnimation class to be useful. You can feed it a CGPath (perhaps one based on the arc segment to travel), and the timing for the animation, and it can make your layer follow the path.
Source: https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CGPath/index.html#//apple_ref/c/func/CGPathAddArc
Source: https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CAKeyframeAnimation_class/index.html
Edit:
Here is some example code for how to draw a partial arc on a CGPath from the current progress to the new progress. I made it work in reverse too. You can play with the numbers and constants, but this is the idea of how to draw an arc segment from a certain percentage to a certain percentage.
Please keep in mind when looking at the CoreGraphics math that it may seem backwards (clockwise vs counterclockwise, etc). This is because UIKit flips everything upside down to put the origin in the upper-left, where CG has its origin in the lower-left.
// start out with start percent at zero, but then use the last endPercent instead
let startPercent = CGFloat(0.0)
// end percent is the "progress" in your code
let endPercent = CGFloat(1.0)
// reverse the direction of the path if going backwards
let clockwise = startPercent > endPercent ? false : true
let minArc = CGFloat(M_PI) * 4/5
let maxArc = CGFloat(M_PI) * 1/5
let arcLength = minArc - maxArc
let beginArc = minArc - (arcLength * startPercent)
let endArc = maxArc + (arcLength * (1.0 - endPercent))
let myPath = CGPathCreateMutable()
CGPathAddArc(myPath, nil, view.bounds.width/2, 0, 160, beginArc, endArc, clockwise)
Here is the full arc segment as defined by the constants minArc and maxArc.
I currently am attempting to make an app that consists of a line that goes straight up, but when the screen is tapped it changes at a 45 degree angle (To the left or right).
I'm creating the path for this like so:
var path = CGPathCreateMutable()
for var i = 0; i < wayPoints.count; i++ {
let p = wayPoints[i]
if i == 0 {
CGPathMoveToPoint(path, nil, p.x, p.y)
} else {
CGPathAddLineToPoint(path, nil, p.x, p.y)
}
}
And I populate the wayPoints array like so (This chunk of code is called a few times every second):
if (wayPoints.isEmpty) {
wayPoints.append(CGPointMake(0, CGRectGetMinY(self.view!.frame)))
} else {
if (wayPoints.count >= 30) {
while (wayPoints.count >= 30) {
wayPoints.removeAtIndex(0)
}
}
if (currentPosition == Position.Right) {
wayPoints.append(CGPointMake(wayPoints.last!.x + 1, wayPoints.last!.y + 1))
} else if (currentPosition == Position.Left) {
wayPoints.append(CGPointMake(wayPoints.last!.x - 1, wayPoints.last!.y + 1))
} else {
wayPoints.append(CGPointMake(0, wayPoints.last!.y + 1))
}
}
(The currentPosition changes when a tap occurs).
And although I don't think this matters, this is how I'm creating the SKShapeNode line:
let shapeNode = SKShapeNode()
shapeNode.path = path
shapeNode.name = "line"
shapeNode.strokeColor = UIColor.blackColor()
This somewhat works, but for some reason it seems as if there is a "leak" (not memory) in the path. This is what it looks like with the above code:
http://gyazo.com/92dd84de15e125ea3d598cbfa9a86b13
As you can see, it makes almost a triangle when the line turns. It should just be a line without all that excess 'mass' on the sides.
The triangle you see is the line path being filled with a color. In this case, black. To remedy this, set the fillColor property of the shape node to the invisible color by
shapeNode.fillColor = SKColor.clearColor()
I used SKColor here instead of UIColor because the former can be used, without modification, in both Mac OS X and iOS apps.
I wish I had more reputation so I could attach a screenshot of what I'm working on but you can see an example in the second link down below.
Basically there is a middle button in the tab bar which brings out option buttons (apple images in this case).
These apple icons are animated to their positions using UIDynamicAnimator and UIAttachmentBehavior.
Here is the code which adds the option buttons
func showOptions() {
var numberOfItems = self.delegate.brOptionsButtonNumberOfItems(self)
//NSAssert(numberOfItems > 0 , "number of items should be more than 0")
var angle = 0.0
var radius:Int = 20 * numberOfItems
angle = (180.0 / Double(numberOfItems))
// convert to radians
angle = angle/180.0 * M_PI
for(var i = 0; i<numberOfItems; i++) {
var csCalc = Float((angle * Double(i)) + (angle/2))
var buttonX = Float(radius) * cosf(csCalc)
var buttonY = Float(radius) * sinf(csCalc)
var wut = (angle * Double(i)) + (angle/2)
var brOptionItem = self.createButtonItemAtIndex(i)
var mypoint = self.tabBar.convertPoint(self.center, fromView:self.superview)
var x = mypoint.x + CGFloat(buttonX)
var y = self.frame.origin.y - CGFloat(buttonY)
var buttonPoint = CGPointMake(x, y)
//println("Button Point of Button Item x:\(x) y: \(y)")
brOptionItem.layer.anchorPoint = self.layer.anchorPoint
brOptionItem.center = mypoint
var attachment = UIAttachmentBehavior(item:brOptionItem, attachedToAnchor:buttonPoint)
attachment.damping = self.damping
attachment.frequency = self.frequency
attachment.length = 1
// set the attachment for dragging behavior
brOptionItem.attachment = attachment
self.dynamicItem!.addItem(brOptionItem)
//if(self.delegate.respondsToSelector("willDisplayButtonItem")) { //Fix me
//self.delegate.brOptionsButton(self, willDisplayButtonItem:brOptionItem)
//}
self.tabBar.insertSubview(brOptionItem, belowSubview: self.tabBar)
self.dynamicsAnimator!.addBehavior(attachment)
self.items.addObject(brOptionItem)
}
}
func createButtonItemAtIndex(indexz:NSInteger) -> BROptionsItem {
println("Create button item at index")
var brOptionItem = BROptionsItem(initWithIndex:indexz)
brOptionItem.addTarget(self, action:"buttonItemPressed:", forControlEvents:.TouchUpInside)
brOptionItem.autoresizingMask = UIViewAutoresizing.None
var image = self.delegate.brOptionsButton(self, imageForItemAtIndex:indexz)
//if((image) != nil) {
brOptionItem.setImage(image, forState: UIControlState.Normal)
//}
/*
var buttonTitle = self.delegate.brOptionsButton(self, titleForItemAtIndex:indexz)
if(buttonTitle.utf16Count > 0) {
brOptionItem.setTitle(buttonTitle, forState:UIControlState.Normal)
}
*/
return brOptionItem;
}
In showOptions the animator is assigned an attachment behavior for a brOptionItem that was just created when createButtomItemAtIndex was called.
In createButtonItemAtIndex, the brOptionItem is created and a target was added to execute a function when the button was tapped.
The option buttons work without the animation. I can click them and the target function is executed.
However, when the animation is added and the option buttons are placed where they are, nothing happens when the buttons are tapped.
I am extremely stuck on this. I have no idea why the animation stops the tapping actions. The buttons are supposed to be draggable as well.
Here is my source code. The code I reference can be see in the BROptiosnButton file.
https://github.com/WobbleDev/BROptionsSwift
Here is the reference code I used
https://github.com/BasheerSience/BROptionsButton
Thanks!
I am using the library AutoScrollLabel for autoScrollLabel like marquee and its working perfect when I need the label to scroll from left to right.
But when I have to use the direction from right to left I have facing this problem:
the label begin from the last word to the first?
Why?
You can find below my code:
self.autoScrollLabel.textColor = [UIColor whiteColor];
self.autoScrollLabel.labelSpacing = 1; // distance between start and end labels
self.autoScrollLabel.pauseInterval = 0;
self.autoScrollLabel.scrollSpeed = 70; // pixels per second
self.autoScrollLabel.textAlignment = NSTextAlignmentCenter;
self.autoScrollLabel.fadeLength = 0;
self.autoScrollLabel.scrollDirection = CBAutoScrollDirectionRight;
self.autoScrollLabel.text = [NSString stringWithFormat:#"%#%#",#"",#"hello hello1 hello1 heeloo3 hello3 hebe hehehe hgdghdhg he e e hehee hehee hehehehehehhe"];
I've never used AutoScrollLabel class, but I think in order to it worked properly you should change property
self.autoScrollLabel.scrollDirection = CBAutoScrollDirectionLeft
when you scroll to the left
for Swift you you can call this method :
func startMarqueeLabelAnimation() {
DispatchQueue.main.async(execute: {
UIView.animate(withDuration: 20.0, delay: 1, options: ([.curveLinear, .repeat]), animations: {() -> Void in
self.marqueeLabel.center = CGPoint(x: 0 - self.marqueeLabel.bounds.size.width / 2, y: self.marqueeLabel.center.y)
}, completion: nil)
})
}