UITextView scrolling without triggering UIPageViewControllerDataSource in UIPageViewController's view controllers - swift

I have multiple view controllers. Something like this:
ViewControllerA
ViewControllerB
ViewControllerC
I use these as my UIPageViewController's view controllers, (as pages).
Each one of them has scrollable UITextViews inside. I cannot disable scrolling because text length should be flexible.
When I scroll the textview, UIPageViewControllerDataSource's viewControllerBefore or viewControllerAfter also is being triggered. How can I prevent this?
Can I disable vertical gestures for UIPageViewController and prevent clashes? Or is there some other way to stop them working at the same time? I want to change page only on horizontal gesture, and scroll the text view only on vertical gesture. But I don't know how to do it. What should I do?

Edit: Thank you #DonMag and #BulatYakupov for trying to help me and warning me about the given information is not enough. I had some restriction on code sharing. Today I found another way. But first here how my screen looks :
I was swiping to turn the page with page curl.
When I scroll the text, it was also triggering viewControllerBefore or viewControllerAfter. It wasn't changing the page, but It was triggering my view controller creating function. This function keep track of current index on an array to supply data to vc creation.
It is something like this:
class myCustomVC: UIViewController {
let textView = UITextView()
}
class PageVCDataService {
var array = ["John","Josh","Joe","Harvy"]
//keeping track of the current index for suppling correct text from array for every page
var currentIndex = 0
func createAfterPage() -> UIViewController {
currentIndex += 1
return createVC(page :array[currentIndex])
}
func createBeforePage() -> UIViewController {
currentIndex -= 1
return createVC(page :array[currentIndex])
}
func createVC(page : String) ->UIViewController {
let vc = myCustomVC()
vc.textView.text = page
return vc
}
}
class PageVC : UIPageViewController {
let dataService = PageVCDataService()
// ...some code
}
extension PageVC: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
return dataService.createBeforePage()
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
return dataService.createAfterPage()
}
}
It wasn't changing the page but it was changing the index. For example if I scroll the text and if my current index at 10 (page 11), my current index was changing to 9. When I swipe for next page, next page was the same page because index was going 9 to 10, which is the same index at the beginning.
So i implemented UITextViewDelegate and used scrollViewDidScroll method. Every time text view scrolls, it send a notification to my data service class, tell it to check if the text on the screen and the text at the current index is same. If it's not, my function find the index for the text on the screen and corrects current index.
It is an ugly solution but maybe it can help someone else.

Related

Cannot modify NSTabViewItem

I may be getting lost in a glass of water as I am not an experienced developer but I cannot seem to be able to implement a simple override to modify the size of an NSTabView item.
I have a Tab View Controller (Style = toolbar)
I have a Tabless Tab View
I have 3 Tab Items. For testing I have only subclassed one of them to the subclass below
I have created a new subclass of NSTabViewItem: MyTabViewItem and subclassed one of the 3 tab Items. The code is:
import Cocoa
class MyTabViewItem: NSTabViewItem {
override func drawLabel(_ shouldTruncateLabel: Bool, in labelRect: NSRect) {
var size = self.sizeOfLabel(false)
size.width = 180
print("Draw!!")
}
override func sizeOfLabel(_ computeMin: Bool) -> NSSize {
var size = super.sizeOfLabel(false)
size.width = 180
print("Draw!!")
return size
}
}
Everything works, except the subclassing. The Tabs appear, they do operate by switching the views and the program runs as it should. Except that it does not resize the Tab Item. The code in the subclass MyTabViewItem is never reached (it never prints Draw!! as it should.
I cannot understand what I am missing here. I have not read of any IB connection to make (and I cannot seem to be able to connect the Tab Items anyways). Please apologise if it isa trivial question but I have searched everywhere and not found anything to help me.
Thank you
You said:
I have a Tabless Tab View
This is your problem. An NSTabView only asks an NSTabViewItem to drawLabel if the NSTabView itself is responsible for drawing the tab bar, but you have a “Tabless” tab view. (“Tabless” is the default style when you drag an NSTabViewController into a storyboard.)
You also said:
I have a Tab View Controller (Style = toolbar)
So you don't even want the tab view to draw a tab bar; you want items in the window toolbar to select tabs (like in Xcode's preference window).
Your ability to customize the toolbar items created for your tabs is limited. You can subclass NSTabViewController and override toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:, like this:
override func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
let toolbarItem = super.toolbar(toolbar, itemForItemIdentifier: itemIdentifier, willBeInsertedIntoToolbar: flag)
if
let toolbarItem = toolbarItem,
let tabViewItem = tabViewItems.first(where: { ($0.identifier as? String) == itemIdentifier.rawValue })
{
toolbarItem.label = "\(tabViewItem.label) 😀"
}
return toolbarItem
}
But I found that making other changes didn't work well:
Setting toolbarItem.image didn't work well for me.
Setting toolbarItem.view made the item stop receiving clicks.
Note that the minSize and maxSize properties are only used if toolbarItem.view is set.
Your best bet is probably to manage the toolbar yourself, without trying to use NSTabViewController's support.
I have also subclassed the NSTabViewController as follows:
import Cocoa
class MyTabViewController: NSTabViewController {
#IBOutlet weak var TradingTabItem: MyTabViewItem!
override func viewDidLoad() {
super.viewDidLoad()
print("Loaded Tab View")
TradingTabItem.label = "New"
// Do view setup here.
}
}
What happens now is that the tab item in my subclass (the only one of the 3 I subclassed) does change its label string to New. However, even if I have added the item as an IBOutlet here, it still does not change seize (and the overridden sizeOfLabel function is not reached).

UICollectionView's prefetchItemsAt not being called

I'm trying to implement data prefetching for my UICollectionView using the UICollectionViewDataSourcePrefetching protocol; however, the respective method is not being called.
The collection view has a custom layout. I have also tried with the regular flow layout, same results.
Upon data reload, I execute the following code to make the collection view have the size of its content to prevent scrolling within that collection view but in a scroll view outside the collection view:
func reloadData() {
viewDidLayoutSubviews()
collectionView.reloadData()
collectionView.layoutIfNeeded()
viewDidLayoutSubviews()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionViewHeightConstraint.constant = collectionView.collectionViewLayout.collectionViewContentSize.height
collectionView.layoutIfNeeded()
view.layoutIfNeeded()
}
Maybe that has something to do with it?
What I have done:
my UIViewController does inherit from UICollectionViewDataSourcePrefetching
collectionView.prefetchDataSource = self (also tried using storyboard)
collectionView.isPrefetchingEnabled = true (also tried using storyboard)
I have implemented collectionView(_:prefetchItemsAt:)
Issue:
The prefetchItemsAt method is not being called. I determined that by placing a print statement in the method & setting up a breakpoint.
Like requested in the comments, I'll share my implementation for this issue here:
I created the tracker prefetchState which determines whether I'm prefetching at the moment, or not:
enum PrefetchState {
case fetching
case idle
}
var prefetchState: PrefetchState = .idle
Then, I hooked up the scroll view's delegate (the scroll view my collection view is in) to my view controller and implemented the scrollViewDidScroll method:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView == self.scrollView else { return }
let prefetchThreshold: CGFloat = 100 // prefetching will start 100pts above the bottom of the scroll view
if scrollView.contentOffset.y > scrollView.contentSize.height-screenBounds.height-prefetchThreshold {
if prefetchState == .idle {
prefetchItems()
}
}
}
In there, you can see that I check whether we're already prefetching. If not, I call prefetchItems(), as implemented here:
func prefetchItems() {
guard prefetchState == .idle else { return }
prefetchState = .fetching
someDownloadFuncWithCompletionBlock { (newItems) in
self.dataSource += newItems
self.collectionView.reloadData()
self.prefetchState = .idle
}
}
I execute the following code to make the collection view have the size of its content to prevent scrolling within that collection view but in a scroll view outside the collection view:
This sounds very broken.
Why are you doing this?
Prefetching on the collection view (from the docs) is triggered when the user scrolls the collection view. By making the collection view frame the same as the content size you are essentially disabling the scrolling of the collection view itself.
The collection view calls this method as the user scrolls
If you are forcing the collection view frame to be the same as the content size then you are entirely breaking UICollectionView.
The reason the prefetch isn't called is because every cell has been loaded already. Nothing is in prefetch any more. Because your collection view is displaying every cell at the same time.
If you want to scroll a collection view... let the collection view handle it. You shouldn't need to place the collection view inside another scroll view.

How to detect when a UIPageViewController's view has changed

I have an app with a PageViewController and I want to make my own custom PageViewController indicator. Everything is all set up. The only thing I need to do now is to figure out how to tell when the view controller's view has changed and what view it is currently on.
I have linked to this demo. In the first part I am clicking on the buttons at the top to change the page and the slider indicating what the current page is working, however after that I swipe my finger to change the controller, and I want to make the slider indicator to move with the page.
Here I had commented what to do for getting current Page index using PageViewController
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool)
{
if (completed)
{
//No need as we had Already managed This action in other Delegate
}
else
{
///Update Current Index
Manager.pageindex = Manager.lastIndex
///Post notification to hide the draggable Menu
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "updateLabelValue"), object: nil)
}
}
//MARK: Check Transition State
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController])
{
///Update Last Index
Manager.lastIndex = Manager.pageindex
///Update Index
Manager.pageindex = pages.index(of: pendingViewControllers[0])!
///Post notification to hide the draggable Menu
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "updateLabelValue"), object: nil)
}
I Had used Notification Observer to Notify the main controller about page Change and Performing some action
Manager is my struct , that hold the current page number , you can make hold the index in struct or in class wherever you want
My struct class
//Struct Class
struct Manager
{
///Current Page Index
static var pageindex :Int = 0
///Last Index
static var lastIndex :Int = 0
}
Main usage
switch Manager.pageindex
{
case 0:
self.mainHomeLabel.text = "VC1"
case 1:
self.mainHomeLabel.text = "VC2"
case 2:
self.mainHomeLabel.text = "VC3"
default:
self.mainHomeLabel.text = "VC1"
}
Note Any queries in the respective code Please ask
Update - Added Demo Project Link
Link - https://github.com/RockinGarg/PageViewController-Demo.git
If you have an array of the pages you're showing then you could use the UIPageViewControllerDelegate to detect page changes. More specifically this function https://developer.apple.com/documentation/uikit/uipageviewcontrollerdelegate/1614091-pageviewcontroller as it will tell you what you are about to transition to. You could then determine what page you are on based on the view controller that is about to be presented.

Adding views with IBAction to a NSStackView crashes application

I want to use the NSStackView to stack views above each other, I also want them to de able to expand so I can't use the NSCollectionView if i understood it correctly.
So, in storyboard, I've created a NSStackView(embedded in scroll view) in the main view controller and a view controller that I want to fill it with:
The button will fill the stack view with ten views:
#IBOutlet weak var stackView: NSStackView!
#IBAction func redrawStackView(_ sender: Any) {
for i in 0..<10 {
let stackViewItemVC = storyboard?.instantiateController(withIdentifier: "StackViewItemVC") as! StackViewItemViewController
stackViewItemVC.id = i
stackView.addArrangedSubview(stackViewItemVC.view)
}
}
And the ViewController on the right simply looks like this:
class StackViewItemViewController: NSViewController {
var id: Int = -1
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
#IBAction func buttonPressed(_ sender: Any) {
debugPrint("StackViewItemViewController" + id.description + "pressed")
}
Running this small application works fine, every time I press the button ten more stack view items appears. But, when I have the audacity to press one of the buttons to the right the application crashes:
Where am I going wrong?
I have tried to work around the IBAction to verify that this what breaks, and the application will not crash if I subclass the button and make a "buttonDelegate" protocol with a function being called from mouseUp.
I guess the problem is that the viewController objects, which you create in the loop, are released immediately.
Even though the view is attached to the stackView, it's viewController is destroyed.
You can fix this issue by keeping a reference to each viewController.
You can do this by creating a new variable
var itemViewControllers = [StackViewItemViewController]()
and then add each newly created viewController to it:
itemViewController.append(stackViewItemVC)

How to push a view in iOS8 and Swift

I am updating an app that pre-dates iOS5 - consequently much needs to change.
In the existing app we push a view (created in a nib) onto a screen that contains a UIPickerView/UIDatePicker to allow the user to make selections.
I this should be an easy thing to migrate to iOS8/Swift but I have spent the past 24h trying to work out the best way to do this with Storyboard/Segue, Container views, viewControllers, UIPopoverPresentationControler etc. with little consensus (even apple's sample code pushes the UIDatePicker in a table cell)
This seems to be a common requirement and I would appreciate any advice/comments on how others have solved this.
Many thanks
Try this
self.performSegueWithIdentifier("your segue identifier", sender: self)
Thanks for the answers and questions seeking clarification.
After a weekend of investigation I realize that I have been stuck in an old iOS paradigm.
With iOS8 everything has moved to ViewController presentation, allowing the developer to control the animation and define behavior on a range of devices (in different orientations).
The definitive answer to my question (I believe) is as follows:
Use UIPresentationController to present the desired UIViewController:
The view controller is the view to display (use code, nib or storyboard to create the picker view)
The presentation controller manages how it is presented (e.g. size, presentationStyle)
This is also the place to handled transparent/blurred views to cover existing content
check out:
func sizeForChildContentContainer(container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize
func frameOfPresentedViewInContainerView() -> CGRect
The UIPresentationController needs a transitioning delegate to manage the presentation (conforms to UIViewControllerTransitioningDelegate)
{
// We need to create a presentation controller to manage the presentation of VC
func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController!, sourceViewController source: UIViewController) -> UIPresentationController? {
let opc = OverlayPresentationController(presentedViewController: presented, presentingViewController: source)
return opc
}
// Get hold of an animation controller for presentation
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animationController = OverlayAnimatedTransitioning()
return animationController
}
// Get hold of an animation controller for dismissal
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animationController = OverlayAnimatedTransitioning()
return animationController
}
which in turn requires an animation controller (UIViewControllerAnimatedTransitioning) to handle the animation (slide, dissolve or something more adventurous):
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
return 0.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let containerView = transitionContext.containerView()
let animationDuration = self .transitionDuration(transitionContext)
containerView.addSubview(toViewController.view)
UIView.animateWithDuration(animationDuration, animations: { () -> Void in
<<< Animation Here>>>)
}) { (finished: Bool) in
transitionContext.completeTransition(true)
}
With the building blocks in place, the desired UIViewController may then be called (using an action/selector/segue) and by setting the transitioningDelegate property of the desired view the desired effect will be invoked:
#IBAction func showOVC(sender: AnyObject) {
let ovc = OverlayViewController()
let overlayTransitioningDelegate = OverlayTransitioningDelegate()
ovc.transitioningDelegate = overlayTransitioningDelegate
self.presentViewController(ovc, animated: true, completion: nil)
}
Dismissal is handled in the same way, simply by calling:
dismissViewControllerAnimated(true, completion: nil)
More information can be obtained via the docs and the excellent WWDC video which made everything clear for me: A look inside presentation controllers
or see apple's example code: LookInside
Bottom line - not as simple as before (assuming a single device in one orientation), but for a little more complexity there is far greater control of views and animation, with the ability to scale to multiple devices.
I hope that this helps people in a similar position
Thanks