Structure of the cell is destroyed after UIView.transition - swift

In one of my controllers I'm using UICollectionView with two-sided cells (there is a button on the face side of the cell, and if user is tapping it cell should show him/her the other side of the cell).
so I have this in my cell class:
firstView = UIView()
...
self.addSubview(firstView)
infoView = UIView()
...
self.insertSubview(infoView, belowSubview: firstView)
Both views have a lot of elements in them, for structuring these elements I'm using mostly Visual Format constraints and anchors.
i.e.
let topFirstViewConstraint = firstView.topAnchor.constraint(equalTo: self.topAnchor, constant: 4)
allConstraints.append(topFirstViewConstraint)
...
let goButtonCenter = goButton.centerXAnchor.constraint(equalTo: firstView.centerXAnchor, constant: 0)
allConstraints.append(goButtonCenter)
For switching between face and rear views I'm using UIView.transition:
UIView.transition(from: firstView, to: infoView, duration: 0.2, options: .transitionFlipFromLeft) { (success) in
}
It works like a charm, I don't have any autoLayout errors at all.
But if I'm trying to switch from firstView to infoView firstView constraints goes wild and vice versa (I still don't have any errors but I know how these views should look, and it's a mess). I've tried to use self.updateConstraints() in transition closure but it doesn't work.
Did anyone experience something like that?

https://developer.apple.com/documentation/uikit/uiview/1622562-transition says:
fromView: The starting view for the transition. By default, this view is removed from its superview as part of the transition.
What happens with constraints when one party of a constraint gets removed from superview? It definitely doesn‘t work anymore.
What you can try
Discussion:
This method provides a simple way to transition from the view in the fromView parameter to the view in the toView parameter. By default, the view in fromView is replaced in the view hierarchy by the view in toView. If both views are already part of your view hierarchy, you can include the showHideTransitionViews option in the options parameter to simply hide or show them.

Related

How to get UIView height and width in Swift? (problem: returning zero)

Sorry in advance for the probably silly question - I'm a beginner
I'm generating a new UIView in viewDidLoad, providing some constraints to anchor it over the main view. When it comes to understand the size of the new view, I always get zero.
Here is a simplified version of my code which is not working:
override func viewDidLoad() {
super.viewDidLoad()
let myView = UIView(frame: .zero)
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
myView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
myView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -200).isActive = true
myView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
myView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
print(myView.bounds.width)
}
The width (but also height) is returning 0.
This happens both with .bounds and .frame.
Do you have any hints?
Thanks!
First question I will ask, does the UIView display on the screen?
Second, from the knowledge of view controller life cycle, the views are laid out in viewDidLayout Subviews.
So, you want to called myView.bounds.width in by overriding the viewDidLayout Subviews or viewDidAppear, when the views have finally appeared in the screens
Thanks for making the question more clear.
I will suggest you either reloadData on the second collection view on viewDidLayout subviews. That way, the two gets layout as it is now, then the second collection view gets layout for the second time after the first collection view is laid.
Another approach will be for you to use relative constraints. I.e. Making weight and height constraints of the items in the second collection view relative to the items in the first collection view. This way, everything else is done automatically for you at runtime. Even if the size of the items in first collection view change later which might affect items of the second collection view, it will be taken care off. (For accurate result if you can get it to work)
I will appreciate if you can share more of the code or screen shots of these two collections views. I could provide a better approach to creating them

TapGesturecognizer not working in backgroundview in iphone X 12.1

I have a simple code to add tapgestureRecognizer to dismis ViewController when tap in screen, but it not working only in iPhone X 12.1.
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(closeSharing(_:))))
Note: magicaly, when i add tap gesturecognizer to a new view that constraint equal to view that also not working, but if i constraint the new view not equal to view, that working. Does any one know why?.
You might need to check a couple of things here:
First step make sure that your main view is not covered for example of another top view which will break your tap gesture, so add the gesture to your top view.
Second step Make sure your view that will have the gesture should have the property view.isUserInteractionEnabled = true, otherwise the gesture will not work.
Third step Make sure your view appears when your testing, you might have a problem with constraints, so the view is out of screen bounds, so try giving the view a backgroundColor = .red to see if it exists or not, or you can use View Debugger from xCode.
Example of a working gesture:
let viewToDismiss = UIView(frame:UIScreen.main.bounds)
viewToDismiss.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(closeSharing(_:)))
tap.numberOfTapsRequired = 1
viewToDismiss.addGestureRecognizer(tap)

How to programmatically set constraints when View sizes are different?

Using Xcode 10, iOS 11.4+, Swift 4
I have a series of UIViewControllers that I am navigating through,
each of which is being placed into a ContainerView.
Based on which VC is pushed, I am hiding/showing the Top or Bottom views (shown in gray) in the Main Controller while the ContainerView is always in the middle (light blue).
What I would like to do is set the constraints so that the ContainerView is appropriately sized when the Top or Bottom views are shown/hidden.
For example, when the "Main Menu" is shown, the ContainerView should fill the entire Main Container view (Top and Bottom views will be hidden)
When "Item1" is shown, the Top view will be shown and the Bottom view hidden. Therefore, ContainerView should fill the Main Container view left, right, and bottom, but the ContainerView Top should be constrained to the Top view bottom.
When "Item5" is shown, the Top and Bottom views will also be shown.
Therefore, ContainerView should fill Main Container view left, right, and be constrained to the Top view bottom, and the Bottom view top (as shown in the Main Container)
I've tried using code like this to fill the entire Main view:
NSLayoutConstraint.activate([
ContainerView.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0),
ContainerView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0),
ContainerView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0),
ContainerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0)
])
However, the ContainerView never changes, and Xcode gives me a lot of warnings like:
[LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one
you don't want.
Here's another screenshot from the Storyboard:
Here's a link to download my sample project:
https://gitlab.com/whoit/containerviews
How can I correctly modify the constraints to accomplish this?
As I've mentioned in the comment, you should have used UIStackView for your top / bottom views visibility controlling.
You will need a UIStackView with following attributes:
Axis: Vertical
Alignment: Fill
Distribution: Fill
Spacing: As per your need
The stack view will contain the Top View, Container View and Bottom View respectively as its sub views.
You need to pin all the sides (leading, trailing, top & bottom) of this stack view to its superview (view controller's view). And as you need some height constraints for your top & bottom view, you give them as your need.
So basically your stack view is now capable of resizing its sub views when you hide any of them. You just need to perform:
theSubViewNeedsTobeHidden.isHidden = true
I've made a working demo for you that can be found here.
Issues
1) You're adding new constraints all the time, as this lines create a new constraints, each time they're getting called:
ContainerView.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0)
2) In the beginning, the MainContainerView is not constraint at all
Solution
I would suggest following:
add those four constraints in the storyboard instead
create IBOutlets for the height-constraints of your top and bottom views.
set the constants of those to 0 (when hiding them)
(optional) for sure you can still fade them in/out by setting their alpha as well, and then add the height constant to the completion block.
Note:
Hidden views (alpha = 0 or isHidden = true) are still taken into account for layouting. That means, your constraints are still valid, when the views are hidden. But to make them look like hidden for the layouting as well, you'll then have to set theirs height constants to 0.
Code
#objc func hideControlViews(_ notification: Notification){
let userInfo = notification.userInfo! as! [String : Bool]
//print("Got top view notification: \(String(describing: userInfo["hide"]))")
if (userInfo["hidetop"]!) {
self.topViewHeightConstraint.constant = 0
} else {
self.topViewHeightConstraint.constant = 64
}
if (userInfo["hidebottom"]!) {
self.bottomViewHeightConstraint.constant = 0
} else {
self.bottomViewHeightConstraint.constant = 108
}
self.view.layoutIfNeeded()
}

How to handle iOS 11 large title animation when using multiple container views?

I am making an app at the moment where 1 screen has a segmented control with 3 segments. Initially I had 1 table view and when you change segment I would simply change the data source/cell etc and reload the table. While this works great there is always the problem that when you change segments it will not remember your last scroll position because the table view gets reloaded.
I tried to get around this with storing offset position, rows etc but I could never get it to work like I wanted. Seems especially annoying when you have different cell types for the segments and they are self sizing as well.
I than decided to have a master view controller with the segmented control and 3 container views with their own VC and table view for each segment. I simply hide/show the correct container view when changing segments. This also works great but I have 1 problem with iOS 11 style large headers. Only the 1st container view added as a subview to the ViewControllers view manipulates the collasping/expanding of the title when you scroll.
Therefore when I change to the 2nd or 3rd container view and start scrolling I do not get the large title collapsing animation. How can I get around that?
I tried the following
1) Change Container view zPosition when changing segments
2) Move the container view to the front by calling view.bringSubview(toFront: ...)
3) Looping through the subviews and calling
view.exchangeSubview(at: 0, withSubviewAt: ...)
I believe I could remove all container views and add the one I need again and give them constraints but I wonder if there is a more straight forward solution.
Or if someone has a good solution to remember a tableViews scroll position before reloading it I would appreciate that too.
So I found an answer that seems to work for me, thanks to this great article. https://cocoacasts.com/managing-view-controllers-with-container-view-controllers/
Essentially what I did is
1) Remove the ContainerViews and Segues from the MasterViewController Storyboard.
2) Add a lazy property for each VC of the segmented control in the MasterViewController. They are lazy so that they only get initialised when you actually need them
lazy var viewController1: LibraryViewController = {
let viewController = UIStoryboard.libraryViewController // convenience property to create the VC from Storyboard
// do other set up if required.
return viewController
}()
lazy var viewController2: LibraryViewController = {
let viewController = UIStoryboard.libraryViewController // convenience property to create the VC from Storyboard
// do other set up if required.
return viewController
}()
3) Create an extension of UIViewController with the following 2 methods. I added them in an extension purely for code organisation as they might be reused on other ViewControllers.
extension UIViewController {
func add(asChildViewController viewController: UIViewController) {
// Add Child View Controller
addChildViewController(viewController)
// Add Child View as Subview
view.addSubview(viewController.view)
// Configure Child View
viewController.view.frame = view.bounds
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// Notify Child View Controller
viewController.didMove(toParentViewController: self)
}
func remove(asChildViewController viewController: UIViewController) {
// Notify Child View Controller
viewController.willMove(toParentViewController: nil)
// Remove Child View From Superview
viewController.view.removeFromSuperview()
// Notify Child View Controller
viewController.removeFromParentViewController()
}
}
4) Now in my segmented control method that gets called when you change segment I simply add the correct ViewController. The nice thing is that remove/adding them does not actually deallocate them.
func didPressSegmentedControl() {
if segmentedControl.selectedSegmentIndex == 0 {
remove(asChildViewController: viewController2)
add(asChildViewController: viewController1)
} else {
remove(asChildViewController: viewController1)
add(asChildViewController: viewController2)
}
}
5) Make sure you call the method at point 4 in ViewDidLoad so that the correct VC is added when the VC is loaded the 1st time.
func viewDidLoad() {
super.viewDidLoad()
didPressSegmentedControl()
}
This way when we remove a ChildViewController and add another one it will always be the the top VC in the subviews array and I get my nice title collapsing animation.
Another added benefit of this approach is that if you never go to a particular segment that particular VC will never get initialised, because they are lazy properties, which should help with efficiency.
Hope this helps somebody trying to do the same.
This is a horrible issue which I hope will be resolved soon, but there is another fix - although I freely admit that this is a nasty hack.
Basically, the issue only applies to the FIRST container view in the hierarchy. Add another container view to your hierarchy. Set it as hidden, then delete its segue and its target view controller just to be neat. Finally, make sure that this is the first container view in the hierarchy by dragging it to the top of the list.

Tracking the position of a NSCell on change

I have a NSTableView and want to track the position of its containing NSCells when the tableView got scrolled by the user.
I couldn’t find anything helpful. Would be great if someone can lead me into the right direction!
EDIT:
Thanks to #Ken Thomases and #Code Different, I just realized that I am using a view-based tableView, using tableView(_ tableView:viewFor tableColumn:row:), which returns a NSView.
However, that NSView is essentially a NSCell.
let cell = myTableView.make(withIdentifier: "customCell", owner: self) as! MyCustomTableCellView // NSTableCellView
So I really hope my initial question wasn’t misleading. I am still searching for a way how to track the position of the individual cells/views.
I set the behaviour of the NSScrollView (which contains the tableView) to Copy on Scroll in IB.
But when I check the x and y of the view/cells frame (within viewWillDraw of my MyCustomTableCellView subclass) it remains 0, 0.
NSScrollView doesn't use delegate. It uses the notification center to inform an observer that a change has taken place. The solution below assume vertical scrolling.
override func viewDidLoad() {
super.viewDidLoad()
// Observe the notification that the scroll view sends out whenever it finishes a scroll
let notificationName = NSNotification.Name.NSScrollViewDidLiveScroll
NotificationCenter.default.addObserver(self, selector: #selector(scrollViewDidScroll(_:)), name: notificationName, object: scrollView)
// Post an intial notification to so the user doesn't have to start scrolling to see the effect
scrollViewDidScroll(Notification(name: notificationName, object: scrollView, userInfo: nil))
}
// Whenever the scroll view finished scrolling, we will start coloring the rows
// based on how much they are visible in the scroll view. The idea is we will
// perform hit testing every n-pixel in the scroll view to see what table row
// lies there and change its color accordingly
func scrollViewDidScroll(_ notification: Notification) {
// The data's part of a table view begins with at the bottom of the table's header
let topEdge = tableView.headerView!.frame.height
let bottomEdge = scrollView.bounds.height
// We are going to do hit-testing every 10 pixel. For best efficiency, set
// the value to your typical row's height
let step = CGFloat(10.0)
for y in stride(from: topEdge, to: bottomEdge, by: step) {
let point = NSPoint(x: 10, y: y) // the point, in the coordinates of the scrollView
let hitPoint = scrollView.convert(point, to: tableView) // the same point, in the coordinates of the tableView
// The row that lies that the hitPoint
let row = tableView.row(at: hitPoint)
// If there is a row there
if row > -1 {
let rect = tableView.rect(ofRow: row) // the rect that contains row's view
let rowRect = tableView.convert(rect, to: scrollView) // the same rect, in the scrollView's coordinates system
let visibleRect = rowRect.intersection(scrollView.bounds) // the part of the row that visible from the scrollView
let visibility = visibleRect.height / rowRect.height // the percentage of the row that is visible
for column in 0..<tableView.numberOfColumns {
// Now iterate through every column in the row to change their color
if let cellView = tableView.view(atColumn: column, row: row, makeIfNecessary: true) as? NSTableCellView {
let color = cellView.textField?.textColor
// The rows in a typical text-only tableView is 17px tall
// It's hard to spot their grayness so we exaggerate the
// alpha component a bit here:
let alpha = visibility == 1 ? 1 : visibility / 3
cellView.textField?.textColor = color?.withAlphaComponent(alpha)
}
}
}
}
}
Result:
Update based on edited question:
First, just so you're aware, NSTableCellView is not an NSCell nor a subclass of it. When you are using a view-based table, you are not using NSCell for the cell views.
Also, a view's frame is always relative to the bounds of its immediate superview. It's not an absolute position. And the superview of the cell view is not the table view nor the scroll view. Cell views are inside of row views. That's why your cell view's origin is at 0, 0.
You could use NSTableView's frameOfCell(atColumn:row:) to determine where a given cell view is within the table view. I still don't think this is a good approach, though. Please see the last paragraph of my original answer, below:
Original answer:
Table views do not "contain" a bunch of NSCells as you seem to think. Also, NSCells do not have a position. The whole point of NSCell-based compound views is that they're much lighter-weight than an architecture that uses a separate object for each cell.
Usually, there's one NSCell for each table column. When the table view needs to draw the cells within a column, it configures that column's NSCell with the data for one cell and tells it to draw at that cell's position. Then, it configures that same NSCell with the data for the next cell and tells it to draw at the next position. Etc.
To do what you want, you could configure the scroll view to not copy on scroll. Then, the table view will be asked to draw everything whenever it is scrolled. Then, you would implement the tableView(_:willDisplayCell:for:row:) delegate method and apply the alpha value to the cells at the top and bottom edges of the scroll view.
But that's probably not a great approach.
I think you may have better luck by adding floating subviews to the scroll view that are partially transparent, with a gradient from fully opaque to fully transparent in the background color. So, instead of the cells fading out and letting the background show through, you put another view on top which only lets part of the cells show through.
I just solved the issue by myself.
Just set the contents view postsBoundsChangedNotifications to true and added an observer to NotificationCenter for NSViewBoundsDidChange. Works like a charm!