iOS PDFKit adjust pdf frame in PDFView - ios

I have a PDF that has pages of different heights that I want to display in a horizontally paged fashion, one page at a time, with no gaps between pages, and the top of each page being aligned with the top of the view. For example:
I would like to use PDFKit's PDFView, especially since these settings can be achieved by:
let pdfView = PDFView()
pdfView.displayDirection = .horizontal
pdfView.displaysPageBreaks = false
pdfView.usePageViewController(true, withViewOptions: nil)
However, this produces the following:
This is due to the varying heights of the pages. Each page is centered vertically in the PDFView, and if its height of the page is greater than the height of the view (e.g. Page 2), it is aspect-scaled to fit.
To attempt to solve the first issue, the vertical offset, I have attempted several things. First, and this is gross and I know it's wrong, after inspecting the view hierarchy and finding the private view (a view of class named PDFTextInputView) that the page is rendered within I've tried adjusting its frame's origin.y to 0 whenever the outer-most scrollview (another private view) scrolls. This works in that whenever the next or previous page is scrolling into view, the page is rendered at the top of the view. However, once the user pinches to zoom on that page, its frame is automatically jumps back to being centered. This approach, aside from relying on private view hierarchy, just won't work.
To attempt to solve the "tall page problem" (Page 2) I've tried adjusting the scale factor of the PDFView whenever a new page appears. Sadly, the .PDFViewPageChanged notification only fires after the scroll view ends decelerating, so the page is zoomed out as it's scrolling in and then when I adjust the scale factor it jumps to fit the width of the screen. Then I turned to the .PDFViewVisiblePagesChanged which does fire as the next/previous pages are coming into view (as well as a few more times, which is fine) and this has the same issue as with .PDFViewPageChanged.
Other things I've tried:
Adjusting the transform on the document view
Intercepting the pinch gesture and adjusting the document view's center
Creating my own page controller and render a single PDF page per page (not using pdfView.usePageViewController)
Does anyone have any ideas of how I can achieve what I'm after?

New Hack:
So it turns out, after digging through some more private APIs (yes, this solution is still gross) that there is this magical property that I had completely overlooked on PDFScrollView (a private, internal view) called... 🥁
🎉 forcesTopAlignment 🎉
To enable this, find the PDFScrollView in your PDFView and:
pdfScrollView.setValue(true, forKey: "forcesTopAlignment")
That does the trick!
Old Hack:
After a few days up against the wall I finally figured out a way to get PDFView to behave how I want.
Disclosure: This solution uses method swizzling and relies on private view hierarchy and could break at any time. That said, it works for now and I'm happy with the solution.
The full solution is available in this gist. (The meat is in overrideCenterAlign.)
There is a private method aptly named _centerAlign which vertically centers and scales the pdf pages as they come onto the screen and as they're scaled with the pinch gesture. Swizzling that method allows me to inject my own logic and apply my own transforms to position the pdf view how I'd like (with the top of the page aligned to the top of the view.)
There are two cases to consider. The "short page" case (pages 1, 3, 4 in the example) and the "tall page" case (page 2 in the example.) In both cases I start by invoking the original implementation of _centerAlign so that it can apply its transforms and manage updating the internal scroll view.
For the "short page" case, I apply the same transform with a vertical translation to align the top of the pdf with the top of the view.
For the "long page" case, I adjust the internal scroll view's zoom scale as it comes onto the screen so that it's scaled to fit the width of the view.
All of that said, I'm open to cleaner solutions that don't rely on private methods and view hierarchy. If you know of a better way to accomplish this, please post an answer!

Just simply set forcesTopAlignment to true
Example:
lazy var pdfView: PDFView = {
let pdf = PDFView()
pdf.displayMode = .singlePageContinuous
pdf.autoScales = true
pdf.translatesAutoresizingMaskIntoConstraints = false
pdf.setValue(true, forKey: "forcesTopAlignment")
return pdf
}()
How my answer is different from accepted one?
There is no property as a scrollView on pdfView available. So you just set value directly to pdfView.

NOTE: this answer is intended to HELP other guys arriving here trying to fix similar problems.
I discovered a nasty bug in iOS12 since apple introduced in iOS13 "drag to
hide" (Apple "https://developer.apple.com/design/human-interface-guidelines/ios/app-architecture/modality/").
Everything works fine if target is iOS13 or above.
In iOS12, maybe due to a different calculated size, (I guess..) pdf is rendered wrong: some contents go to right and is clipped away.
A dirty fix consists in don't calling
self?.pdfView.autoScales = true
in viewDidLoad,
but after loading every PDF waiting some mS AND set it:
if #available(iOS 13.0, *) {
// NADA.. go on..
}else{
self.pdfView.autoScales = false // disable it.
self?.pdfView.alpha = 1
}
let pdf = load .......
self.pdfView.document = pdf
if #available(iOS 13.0, *) {
//nada..
} else {
// hack for ios 12
let when = DispatchTime.now() + 0.25
DispatchQueue.main.asyncAfter(deadline: when, execute: { [weak self] in
self?.pdfView.autoScales = true // hack for ios 12
self?.pdfView.alpha = 1
})
}
Use alpha to prevent viewing pdf not adopted yet.

I think the problem is with adding PDFView to view with constraint. When I added using just addSubview() and user proper frame, it does not require to call setValue.
let pdfView = PDFKit.PDFView(frame: self.view.bounds)
pdfView.document = PDFKit.PDFDocument(url: url)
pdfView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
view.addSubview(pdfView)

Related

Embedding Intrinsically Sized View Breaks Animation

I have built a test project to show what the goal is vs. what I currently have happening. The gif on the left shows exactly what I want the ending appearance to be. It is constructed with a single traditional view hierarchy. I need to achieve this with the pink view being an embedded/contained view. My attempts so far have only gotten me to the gif on the right.
The way the (pink) contained view grows is possibly an important detail: the blue subview changes it's height, and the whole apparatus gets a new intrinsic size because of all the connected vertical constraints. As you would expect, this is a simplification of my actual app, but I think it has all the important bits.
The main things I see that are strange:
The yellow/orange "other" view is not animating at all.
The pink contained view is animating nicely for it's own part, but it is animating it's position, even though it's frame has the same origin before and after the animation as shown here:
Here is the Storyboard of the right gif. Both the container view in the "parent" scene and the top view in the "child" scene have translatesAutoresizingMaskIntoConstraints set to false with runtime attributes.
The question then: **What must I change about my configuration to get all affected layout changes to animate (properly) when I have a size change in an intrinsically-sized and contained view? **
Edit: Tried manual embed
Since posting the question, I have tried a manual View Controller Containment strategy, and I got the exact same results as with the Storyboard technique, which is ultimately a good sign for the platform. There was 1 fewer view in the total hierarchy, but it didn't seem to make a difference.
Edit: Bounty and project
I have added a 100 point bounty to attract attention. I have also uploaded my sample project to this github repo. Check it out!
Changing your animation block in InnerViewController as follows does the trick.
var isCollapsed = false {
didSet {
let factor:CGFloat = isCollapsed ? 1.5 : 0.66
let existing = innerViewHeightConstraint.constant
UIView.animate(withDuration: 1.0) {
self.innerViewHeightConstraint.constant = existing * factor
self.view.layoutIfNeeded()
self.parent?.view.layoutIfNeeded()
}
}
}
The key difference is self.parent?.view.layoutIfNeeded(), which tells the embedding view controller to update the constraints as part of the animation, instead of immediately before the start of the animation.

UIScrollView: View inside don't fit properly, weird behaviour, view snaps back on touch

I am having trouble with a UIScrollView... I have researched a lot and found cases that were similar yet the solutions never seemed to work so here's a little explanation (it's quite hard to explain)
I have a viewcontroller that contains a UIScrollView and I load a couple of view controllers inside that uiscrollview... I instatnite them and then I add them to the scrollView like this...
viewController1.view.frame = scrollView1.bounds
var frame0 = viewController1.view.frame
frame0.origin.x = 0
viewController1.view.frame = frame0
self.addChildViewController(viewController1)
self.scrollView1.addSubview(viewController1.view)
viewController1.didMoveToParentViewController(self)
viewController2.view.frame = scrollView1.bounds
var frame01 = viewController2.view.frame
frame01.origin.x = self.view.frame.size.width
viewController2.view.frame = frame01
self.addChildViewController(viewController2)
self.scrollView1.addSubview(viewController2.view)
viewController2.didMoveToParentViewController(self)
...
after that I configure the scroll view as follows
self.scrollView1.contentSize = CGSizeMake(self.view.frame.width * 5, self.view.frame.height)
scrollView1.delaysContentTouches = false
Times 5 because there are 5 ViewControllers
(The scroll view has paging enabled)
Now this method works quite well and all views are loaded inside the scroll view and I can swipe trough my views however here's the problem.
My first view is loaded fine, then when I swipe to the right I see that the second view is offset to the top and a part of it is hidden, then when I touch a button inside the view it suddenly snaps to the bottom in it's place... This is really annoying because it makes the user interface really irritating since a part of it is hidden...
I have made an illustration of it: http://imgur.com/xHRwXt4
I have tried a bunch of things like setting
self.automaticallyAdjustsScrollViewInsets = false
or
self.scrollView1.clipsToBounds = true
but still the same problem occurs. I also thaught it was my navigation bar since the view containing the scrollview is located inside a ViewController that has a navigation bar (from uinavigationcontroller)
So for testing purposes I tried it without a navigation controller and bar but still the same kind of problem occurred. It's really frustrating and I can't seem to find a solution.
However I do have to say when the scrollview was located inside the ViewController with navigationBar and I set
self.scrollView1.contentSize = CGSizeMake(self.view.frame.width * 5, self.view.frame.height - 66)
(Notice the -66)
Then the snapping problem didn't occur, however this wasn't the effect I was looking for since the scrollview needed to fill up the whole view, also the view inside the UIScrollView wasn't completely visible then so that wasn't a solution either.
I also taught it was maybe because my constraints were differently set up on one of the view controllers I loaded so I tried it with initiating the same type of view controller a few times still the same problem occurred...
(So I have tried a lot but no matter what I try to do it gets worse or another problem occurs for each view moves in either direction then I add self.automaticallyAdjustsScrollViewInsets = false but then the snapping returns etc. etc.)
Hopefully somebody has some suggestions to solve this
(If I you need some more illustrations etc. then I will provide these too.)
Thank You
From the looks of it, there are 2 things to try:
Some of child view controllers may contain scroll views and their automaticallyAdjustsScrollViewInsets = false needs to be set other than the parent view controller
A very helpful way to find out is to turn on view debugging. Run the project, then go to Xcode > View Debugging > Show View Frames. See if the child views of the child view controllers are misaligned along the top. If they are properly aligned (top all flushed), it means the problem lies in the individual child views.

UIStackView - layout constraint issues when hiding stack views

My app has 2 screens:
TableViewVC (no stack views here)
DetailVC (all the nested stack views here; please see link for picture: Nested StackViews Picture) -- Note, there are labels and images within these stack views.
When you press a cell in the tableview, it passes the information from the TableViewVC to the DetailVC. The problem is with hiding the specific UIStackViews in the DetailVC. I want only 2 stack views out of the various ones in the DetailVC to be hidden as soon as the view loads. So I write this code in the DetailVC to accomplish this:
override func viewDidLoad() {
super.viewDidLoad()
self.nameLabel.text = "John"
self.summaryStackView.hidden = true
self.combinedStackView.hidden = true
}
Everything looks great but Xcode give many warnings only at runtime. There are no warning in Storyboard when the app is not running. Please see link for picture of errors: Picture of Errors
Basically it's a lot of UISV-hiding, UISV-spacing, UISV-canvas-connection errors. These errors go away if I hide the same stack views in viewDidAppear but then there is a flash of the stuff that was supposed to be hidden and then it hides. The user sees the the view briefly and then it hides which is not good.
Sorry for not being able to actually post pictures instead of links, still can't do so.
Any suggestions on how to fix this? This is for an app I actually want to launch to the app store - it's my first so any help would be great!
Edit/ Update 1:
I found a small work around with this code which I put inside the second screen called DetailVC:
// Function I use to delay hiding of views
func delay(delay: Double, closure: ()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
// Hide the 2 stack views after 0.0001 seconds of screen loading
override func awakeFromNib() {
delay(0.001) { () -> () in
self.summaryStackView.hidden = true
self.combinedStackView.hidden = true
}
}
// Update view screen elements after 0.1 seconds in viewWillAppear
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
delay(0.1) { () -> () in
self.nameLabel.text = "John"
}
}
This gets rid of the warnings about layout constraints completely from Xcode.
It's still not perfect because sometimes I see a glimpse of the views that are supposed to be hidden -- they flash really quick on the screen then disappear. This happens so quickly though.
Any suggestions as to why this gets rid of warnings? Also, any suggestions on how to improve this to work perfectly??? Thanks!
I had the same problem and I fixed it by giving the height constraints of my initially hidden views a priority of 999.
The problem is that your stackview applies a height constraint of 0 on your hidden view which conflicts with your other height constraint. This was the error message:
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0x7fa3a5004310 V:[App.DummyView:0x7fa3a5003fd0(40)]>",
"<NSLayoutConstraint:0x7fa3a3e44190 'UISV-hiding' V:[App.DummyView:0x7fa3a5003fd0(0)]>"
)
Giving your height constraint a lower priority solves this problem.
This is a known problem with hiding nested stack views.
There are essentially 3 solutions to this problem:
Change the spacing to 0, but then you'll need to remember the previous spacing value.
Call innerStackView.removeFromSuperview(), but then you'll need to remember where to insert the stack view.
Wrap the stack view in a UIView with at least one 999 constraint. E.g. Top, Leading, Trailing # 1000, Bottom#999.
The 3rd option is the best in my opinion. For more information about this problem, why it happens, the different solutions, and how to implement solution 3, see my answer to a similar question.
You can use the removeArrangedSubview and removeFromSuperview property of UIStackView.
In Objective-C :
[self.topStackView removeArrangedSubview:self.summaryStackView];
[self.summaryStackView removeFromSuperview];
[self.topStackView removeArrangedSubview:self.combinedStackView];
[self.combinedStackView removeFromSuperview];
From Apple UIStackView Documentation:(https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIStackView_Class_Reference/#//apple_ref/occ/instm/UIStackView/removeArrangedSubview:)
The stack view automatically updates its layout whenever views are added, removed or inserted into the arrangedSubviews array.
removeArrangedSubview: This method removes the provided view from the stack’s arrangedSubviews array. The view’s position and size will no longer be managed by the stack view. However, this method does not remove the provided view from the stack’s subviews array; therefore, the view is still displayed as part of the view hierarchy.
To prevent the view from appearing on screen after calling the stack’s removeArrangedSubview: method, explicitly remove the view from the subviews array by calling the view’s removeFromSuperview method, or set the view’s hidden property to YES.
When the UIViewStack is hidden, the constraints automatically generated by the UIStackView will throw lots of UISV-hiding, UISV-spacing, UISV-canvas-connection warnings, if the UIStackView's spacing property has any value other than zero.
This doesn't make much sense, it's almost certainly a framework bug. The workaround I use is to set the spacing to zero when hiding the component.
if hideStackView {
myStackView.hidden = true
myStackView.spacing = CGFloat(0)
} else {
myStackView.hidden = false
myStackView.spacing = CGFloat(8)
}
I've found that nested UIStackViews show this behavior if you set the hidden property in ✨Interface Builder✨. My solution was to set everything to not hidden in ✨Interface Builder✨, and hide things in viewWillAppear selectively.
This error is not about hiding, but about ambiguous constraints. You must not have any ambiguous constraints in your view.
If you add them programmatically you should exactly understand what constraints you add and how they work together.
If you do not add them programmatically, but use storyboard or xib, which is a good place to start, make sure there are no constraint errors or warnings.
UPD: You have a pretty complex structure of views there. Without seeing the constraints is hard to say what exactly is wrong. However, I would suggest to build you view hierarchy gradually adding views one by one and making sure there are no design-time/runtime warnings.
Scroll view may add another level of complexity if you do not handle it correctly. Find out how to use constraints with a scroll view.
All other timing hacks is not a solution anyway.
I moved all UIStackView.hidden code from viewDidLoad to viewDidAppear and broken constraints problem went away. In my case all conflicting constraints were auto generated, so no way to adjust priorities.
I also used this code to make it prettier:
UIView.animateWithDuration(0.5) {
self.deliveryGroup.hidden = self.shipVia != "1"
}
EDIT:
Also needed the following code to stop it from happening again when device is rotated:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
self.deliveryGroup.hidden = false
coordinator.animateAlongsideTransition(nil) {
context in
self.deliveryGroup.hidden = self.shipVia != "1"
}
}
I fixed it by putting the hide commands in traitCollectionDidChange.
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
self.item.hidden = true
}
So, this may only help 0.000001% of users but maybe this is a clips to bounds issue.
I ran into this recently when working with UICollectionViewCell I forgot to check clips to bounds on the view I was treating as my content view. When you create a UITableViewCell in IB it sets up a content view with clips to bounds as the default.
Point is, depending on your situation you may be able to accomplish your intended effect using frames and clipping.
Put your hide commands in viewWillLayoutSubviews() {}
I did this by storing all the hidden views of the nested UIStackView in an array and removing them from the superview and arranged subviews. When I wanted them to appear again I looped through the array and added them back again. This was the first step.
The second step is after you remove the views of the nested UIStackView from the superview the parent UIStackView still doesn't adjust it's height. You can fix this by removing the nested UIStackView and adding it again straight afterwards:
UIStackView *myStackView;
NSUInteger positionOfMyStackView = [parentStackView indexOfObject:myStackView];
[parentStackView removeArrangedSubview:myStackView];
[myStackView removeFromSuperview];
[parentStackView insertArrangedSubview:myStackView atIndex:positionOfMyStackView];
If you're having issues animating HIDING AND SHOWING subviews at the same time, repeating the .isHidden instructions in the animation completion may help. See my answer here for more detail on that.
Have you tried this? Calling super after your changes?
override func viewWillAppear() {
self.nameLabel.text = "John"
self.summaryStackView.hidden = true
self.combinedStackView.hidden = true
super.viewWillAppear()
}

iOS WebView blank gap

I have embedded a Youtube video in my iOS 8.3 app using youtube's standard embed url (sample) which is working as expected but looking pretty weird. That white gap (blue zone on the view tree inspector) should not be there.
I can confirm it's not a CSS issue as the inspector shows it is effectively occupying 100% width and height of the uiWebView component.
I have setup constraints to the left, top and right borders to equate the window/layout guides so that's not the problem either. It seems a private _UIWebViewScrollView component is taking all the height i assigned to the UIWebView, and pushing the real content (UIWebViewBrowserView) to the bottom of said area.
I looked up info on the scroll view class but it seems there's only header files scattered on the web with no real apple docs so i'd rather not mess with that scroll view if possible.
Seems my hunch is right, the same problem was described by someone on another question also regarding a scrollview but the mentioned function to fix the problem either does not exist anymore or is now private. I'm still looking for answers.
I fixed it.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews();
playerWebView.scrollView.contentInset = UIEdgeInsetsZero;
}
Adding to the edit i made to the question, i forgot swift changes set and get methods for accessor variables as in C#.
Swift 3: Removing the default blank top space in UIWebView
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews();
webView.scrollView.contentInset = UIEdgeInsets.zero;
}
The accepted solutions did not work for me. Instead it was just because there was space for the navigation bar title text that I was not using. I got rid of it by adding displayMode: .inline.
var body: some View {
VStack {
WebView(request: URLRequest(url: URL(string: url!)!))
}.navigationBarTitle(Text("Title"), displayMode: .inline)
}
Update for swift 3
webViewResetPassw.scrollView.contentInset = UIEdgeInsets.zero

Xamarin iOS How to Zoom on UIView and UITableView

I am developping an iPad app using Xamaring that has several views in the storyboard (UIView and UITableView). My users want to zoom on usual pages (as they are used to in a web browser or so).
So, having read about UIScrollView, I simply tried to put my page views embedded within a UIScrollView, but I can neither scroll nor pinch-zoom: nothing happens.
As for the setup: in the StoryBoard, I add a UIScrollView to the UIViewController which fills the parent. I then add content to the UIScrollView, which I want to be zoomable (e.g. for people with poor sight).
So the question is quite simple: how can I get a view fitting within its original parent but that can be zoomed onto ?
Thanks in advance!
I have never done this myself, so I dont know if this is all it takes. But have you added the max- and min-scroll and all that?
scrollView.MaximumZoomScale = 3f;
scrollView.MinimumZoomScale = .1f;
scrollView.ViewForZoomingInScrollView += (UIScrollView sv) => { return imageView; };
(in your case i guess it needs to return the view);
If that doesent do the trick, take a look at "Scrolling and zooming a view hierarchy on this page: http://www.raywenderlich.com/10518/how-to-use-uiscrollview-to-scroll-and-zoom-content

Resources