UIButton exceeding view controller without clipping to its bounds - ios

In my app I'm using KYDrawerController library from here: https://github.com/ykyouhei/KYDrawerController
It works as expected, but I want to add an UIButton on the menu view controller (which is on top when opened), however it's clipped by view bounds. Best way I can explain this is by showing you a screenshot:
And here's how it should look like:
Button now has negative right constraint margin so it's position is correct, but how can I disable clipping?
In the menu view controller, which can you see on the foreground, I've added this code:
self.navigationController?.navigationBar.clipsToBounds = false
self.navigationController?.view.clipsToBounds = false
self.view.clipsToBounds = false
let elDrawer = self.navigationController?.parent as! KYDrawerController
elDrawer.view.clipsToBounds = false
elDrawer.displayingViewController?.view.clipsToBounds = false
elDrawer.drawerViewController?.view.clipsToBounds = false
elDrawer.displayingViewController?.view.clipsToBounds = false
elDrawer.mainViewController.view.clipsToBounds = false
elDrawer.inputViewController?.view.clipsToBounds = false
elDrawer.splitViewController?.view.clipsToBounds = false
As you can see I've tried all possible ways to disable clipping, yet it's still clipped. How can I achieve this?
edit:
Added hierachy view:
I've also tried to run following test:
var view = arrowButton as UIView?
repeat {
view = view?.superview
if let sview = view {
if(sview.clipsToBounds){
print("View \(view) clips to bounds")
break
}
else{
print("View \(view) does not clip to bounds")
}
}
} while (view != nil)
And it prints:
View Optional(>) does not clip to
bounds
So looks like nothing is clipping yet it's clipped.
edit2:
debug view hierarchy:

Yay, I've found the solution:
self.navigationController?.view.subviews[0].clipsToBounds = false
old code is not needed.
UINavigationTransitionView (it's Apple private class) was the one responsible with clipToBounds turned on.

We can't really tell what's going on because you don't show us the view hierarchy.
I suggest you write test code that starts at the button, walking up the superview hierarchy looking for a superview who's' clipsToBounds is true. Something like this:
var view = button as UIView?
repeat {
view = view.superview
if view?.superview.clipsToBounds ?? false == true {
print("View \(view) clips to bounds")
break
} while view != nil
Then you'll know which view is clipping, and you can fix it witout the "shotgun" approach you're using.
EDIT:
I'm not sure why you are getting such strange debug messages. I suggest adding unique tag numbers to all the button's superviews, and then using code like this:
override func viewDidAppear(_ animated: Bool) {
var view = button as UIView?
repeat {
view = view?.superview
if let view = view {
let tag = view.tag
let description = (tag == 0) ? view.debugDescription : "view w tag \(tag)"
if(view.clipsToBounds){
print("View \(description) clips to bounds")
break
}
else{
print("View \(description) does not clip to bounds")
}
}
} while (view != nil)
}
Post ALL the output from that debug code so we can see the entire view hierarchy you're dealing with.

Related

Increase the height of UINavigationBarLargeTitleView

I want to add a banner to the navigation bar, but by increasing the height of it. I want to copy the design and behaviour of an artist page in the Apple Music app:
It behaves just like a normal Large Title would, except for that it has been moved down, it has a sticky UIImageView behind it and it returns its background when the user scrolls down far enough. You can fire up Apple Music, search for an artist and go to their page to try it out yourself.
I've tried a bunch of things like setting the frame on the UINavigationBarLargeTitleView, and the code from this answer: https://stackoverflow.com/a/49326161/5544222
I already got a hold of the UINavigationBarLargeTitleView and its UILabel using the following code:
func setLargeTitleHeight() {
if let largeTitleView = self.getLargeTitleView() {
if let largeTitleLabel = self.getLargeTitleLabel(largeTitleView: largeTitleView) {
// Set largeTitleView height.
}
}
}
func getLargeTitleView() -> UIView? {
for subview in self.navigationBar.subviews {
if NSStringFromClass(subview.classForCoder).contains("UINavigationBarLargeTitleView") {
return subview
}
}
return nil
}
func getLargeTitleLabel(largeTitleView: UIView) -> UILabel? {
for subview in largeTitleView.subviews {
if subview.isMember(of: UILabel.self) {
return (subview as! UILabel)
}
}
return nil
}
Initially put a view with image and label and play button. Then clear the navigation bar it will show the image below it by
self.navigationController!.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController!.navigationBar.shadowImage = UIImage()
self.navigationController!.navigationBar.isTranslucent = true
later when you scroll up you have to handle it manually and again show the
navigation with title.

Weird underlying gray outlined view trying to dismiss programmatically the master of UISplitViewController

I use a UISplitViewController with preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay and I was looking for a way to dismiss master view controller. My master contains a table view and I'd like to close it whenever I select a cell. Surprisingly UISplitViewController doesn't seem to offer a method for that (but I do see Apple Mail doing that we you select an email in portrait mode).
I found the following workaround reported here: Hiding the master view controller with UISplitViewController in iOS8 (look at phatmann answer). This works but it also creates a weird animation when it's dismissed, there's an underlying gray outlined view which is not animated together with my master's view. The problem has been reported also here: iOS Swift 2 UISplitViewController opens detail screen on master's place when on iPad/ iPhone 6+
The problem occurs only when I dismiss master with this workaround, not when I tap on the secondary so I guess UISplitViewController is not following the regular dismiss flow when you just call sendAction on the button.
I used the following code to address this problem. There might be a better way to match on the specific view that is causing the issue. That said, this code was in an app approved by Apple in April of this year. What the code does is look for a specific view of a certain type, and if found, then it makes it hidden until the animation is complete. Its somewhat future proof, since if it does't detect the special view, it does nothing. I also added some comments for adopters on where you might want to make changes.
func closePrimaryIfOpen(finalClosure fc: (() -> Void)? = nil) {
guard let
primaryNavController = viewControllers[0] as? MySpecialNavSubclass,
primaryVC = primaryNavController.topViewController as? MySpecialCalss
else { fatalError("NO Special Class?") }
// no "official" way to know if its open or not.
// The view could keep track of didAppear and willDisappear, but those are not reliable
let isOpen = primaryVC.view.frame.origin.x >= -10 // -10 because could be some slight offset when presented
if isOpen {
func findChromeViewInView(theView: UIView) -> UIView? {
var foundChrome = false
var view: UIView! = theView
var popView: UIView!
repeat {
// Mirror may bring in a lot of overhead, could use NSStringFromClass
// Also, don't match on the full class name! For sure Apple won't like that!
//print("View: ", Mirror(reflecting: view).subjectType, " frame: \(view.frame)")
if Mirror(reflecting: view).description.containsString("Popover") { // _UIPopoverView
for v in view.subviews {
//print("SV: ", Mirror(reflecting: v).subjectType, " frame: \(v.frame)")
if Mirror(reflecting: v).description.containsString("Chrome") {
foundChrome = true
popView = v
//popView.hidden = true
break
}
}
if foundChrome { break }
}
view = view.superview
} while view != nil
return popView
}
// Note: leave it as optional - Apple changes things and we don't find the view, things still work!
let chromeView = findChromeViewInView(self.view)
UIView.animateWithDuration(0.250, animations: {
chromeView?.hidden = true
self.preferredDisplayMode = .PrimaryHidden
}, completion: { Bool in
self.preferredDisplayMode = .PrimaryOverlay
chromeView?.hidden = false
if let finalClosure = fc {
finalClosure()
}
//print("SLIDER CLOSED DONE!!!")
} )
}
}

Slide UIInputView with UIViewController like Slack

I'd like to use the UIViewController's input accessory view like this:
override func canBecomeFirstResponder() -> Bool {
return true
}
override var inputAccessoryView: UIView! {
return self.bar
}
but the issue is that I have a drawer like view and when I slide the view open, the input view stays on the window. How can I keep the input view on the center view like Slack does it.
Where my input view stays at the bottom, taking up the full screen (the red is the input view in the image below):
There are two ways to do this exactly like Slack doing it, Meiwin has a medium post here A Stickler for Details: Implementing Sticky Input Field in iOS to show how he managed to do this which he actually puts an empty UIView as an inputAccessoryView then track it’s coordinates on screen to know where to put his custom view in relation with the empty view, this way can be helpful if you are going to support SplitViewController on iPad, but if you are not interested in this way, you can see how I managed to do this like this image
Here is before swiping
Here is after
All I did was actually taking a snapshot from the inputAccessoryView window and putting it on the NavigationController of the TableViewController
I am using SideMenu from Jon Kent and it’s pretty easy to do it with the UISideMenuNavigationControllerDelegate
var isInputAccessoryViewEnabled = true {
didSet {
self.inputAccessoryView?.isHidden = !self.isInputAccessoryViewEnabled
if self.isInputAccessoryViewEnabled {self.becomeFirstResponder()}
}
}
func sideMenuWillAppear(menu: UISideMenuNavigationController, animated: Bool) {
let inputWindow = UIApplication.shared.windows.filter({$0.className == "UITextEffectsWindow"}).first
self.inputAccessoryViewSnapShot = inputWindow?.snapshotView(afterScreenUpdates: false)
if let snapShotView = self.inputAccessoryViewSnapShot, let navView = self.navigationController?.view {
navView.addSubview(snapShotView)
}
self.isInputAccessoryViewEnabled = false
}
func sideMenuDidDisappear(menu: UISideMenuNavigationController, animated: Bool) {
self.inputAccessoryViewSnapShot?.removeFromSuperview()
self.isInputAccessoryViewEnabled = true
}
I hope that helps :)

view.traitCollection.horizontalSizeClass returning undefined (0) in viewDidLoad

I'm using a UITextView inside a UIPageViewController, and I want to determine the font size based on the size class of the device.
The first slide of the page view is loaded in ViewDidLoad like so (viewControllerAtIndex(0)):
override func viewDidLoad() {
super.viewDidLoad()
//Some unrelated code here
// Page View Controller for Questions Slider
questionPageVC = storyboard?.instantiateViewControllerWithIdentifier("QuestionPageView") as? UIPageViewController
questionPageVC!.dataSource = self;
questionPageVC!.delegate = self;
let startingViewController : QuestionContentViewController = viewControllerAtIndex(0) as QuestionContentViewController
var viewControllers = [startingViewController]
questionPageVC!.setViewControllers(viewControllers, direction: .Forward, animated: true, completion: nil)
let sliderHeight = view.frame.size.height * 0.5
questionPageVC!.view.frame = CGRectMake(20, 70,
view.frame.size.width-40, sliderHeight)
addChildViewController(questionPageVC!)
view.addSubview(questionPageVC!.view!)
questionPageVC?.didMoveToParentViewController(self)
var pageControl : UIPageControl = UIPageControl.appearance()
pageControl.pageIndicatorTintColor = UIColor.lightGrayColor()
pageControl.currentPageIndicatorTintColor = UIColor.blackColor()
pageControl.backgroundColor = UIColor.whiteColor()
// Some more code here
}
And then, in viewControllerAtIndex:
private func viewControllerAtIndex(index: Int) -> QuestionContentViewController {
var pcvc : QuestionContentViewController = storyboard?.instantiateViewControllerWithIdentifier("QuestionContentView") as! QuestionContentViewController
var fontSize = ""
if (view.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.Compact) {
fontSize = "20"
} else {
fontSize = "28"
}
pcvc.questionString = TextFormatter(string: fontSize + questionsArray[index]).formattedString
pcvc.questionIndex = index
return pcvc
}
The problem is that the very first slide, which was called in viewDidLoad, always uses the font size in the "else" clause.
If I print view.traitCollection.horizontalSizeClass, for that first slide, I get 0 (UIUserInterfaceSizeClassUnspecified), for subsequent slides, I get the correct size.
I tried moving the whole thing to "viewWillAppear", and then weird things happen to the UIPageViewController (an extra slide with the wrong size text behind the other slides)
The problem is that viewDidLoad is too soon to be asking about a view's trait collection. This is because the trait collection of a view is a feature acquired from the view hierarchy, the environment in which it finds itself. But in viewDidLoad, the view has no environment: it is not in in the view hierarchy yet. It has loaded, meaning that it exists: the view controller now has a view. But it has not been put into the interface yet, and it will not be put into the interface until viewDidAppear:, which comes later in the sequence of events.
However, the view controller also has a trait collection, and it does have an environment: by the time viewDidLoad is called, the view controller is part of the view controller hierarchy. Therefore the simplest (and correct) solution is to ask for the traitCollection of self, not of view. Just say self.traitCollection where you now have view.traitCollection, and all will be well.
(Your solution, asking the screen for its trait collection, may happen to work, but it is not reliable and is not the correct approach. This is because it is possible for the parent view controller to alter the trait collection of its child, and if you bypass the correct approach and ask the screen, directly, you will fail to get the correct trait collection.)
I found that if I use the main screen's traitCollection, instead of the current view, I get the correct size class:
if (UIScreen.main.traitCollection.horizontalSizeClass == .compact) {
fontSize = "20"
} else {
fontSize = "28"
}
You will be better off moving that code to the viewWillAppear method, as in the viewDidLoad the ViewController's view has not been added to the hierarchy yet, and you might get an empty trait collection.
In my case I needed my view to know about the horizontalSizeClass so accessing the UIScreen traitCollection was tempting but not encouraged, so I had something like this:
override func layoutSubviews() {
super.layoutSubviews()
print("\(self.traitCollection.horizontalSizeClass.rawValue)")
switch self.traitCollection.horizontalSizeClass {
case .regular, .unspecified:
fontSize = 28
case .compact:
fontSize = 20
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
print("\(self.traitCollection.horizontalSizeClass.rawValue)")
guard let previousTraitCollection = previousTraitCollection else { return }
if self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass {
layoutIfNeeded()
}
}
I took a hint from this Apple Technical Q&A and used viewWillLayoutSubviews instead of viewDidLoad/viewWillAppear.

Moving UIView to new superview and back again from UITableViewCell

I have a custom UITableViewCell which has a custom UIView within it. When someone presses on the UITableViewCell, the UIView is transferred as a subview to a new UIWindow. Then when the UIWindow is dismissed I need the UIView to be transferred back into the same UITableViewCell that was selected.
I have tried using this code:
(self.tableView.cellForRowAtIndexPath(self.tableView.indexPathForSelectedRow()!) as! ContentTableCell).replaceControls(self.expandedViewController.Controls)
where replaceControls() is a function inside my custom table cell that adds the given view as a subview and connects it to the appropriate #IBOutlet. self.expandedViewController.Controls is the custom UIView.
However this doesn't work and produces the error CGAffineTransformInvert: singular matrix.
What about putting all the objects which are supposed to be set in two views in single one view with the hidden function and CATrasition.
For example, you create the instance of UIButton with the #IBAction fun flipViews(). And you shall set the objects which are supposed to be set in BackSideView with hidden in your viewDidLoad() or your storyboard.
func flipViews() {
UIView.beginAnimations("ViewSwitch", context: nil)
UIView.setAnimationDuration(0.6)
UIView.setAnimationCurve(.EaseInOut)
if backSideObject1.hidden {
frontSideObject1.hidden = true
frontSideObject2.hidden = true
frontSideObject3.hidden = true
backSideObject1.hidden = false
backSideObject2.hidden = false
backSideObject3.hidden = false
} else {
frontSideObject1.hidden = false
frontSideObject2.hidden = false
frontSideObject3.hidden = false
backSideObject1.hidden = true
backSideObject2.hidden = true
backSideObject3.hidden = true
}
UIView.setAnimationTransition(.FlipFromLeft, forView: view, cache: true)
UIView.commitAnimations()
}
I hope my answer can help you.

Resources