How to create content sized popover in Swift - ios

I cannot find a way to present a popover view that is just as big as its contents. I'm using the following code:
func show_popup(_ popVC: UIViewController, sender: UIButton)
{
let v = sender
popVC.modalPresentationStyle = .popover
let popOverVC = popVC.popoverPresentationController
popOverVC?.delegate = self
popOverVC?.sourceView = v
popOverVC?.sourceRect = CGRect(x: v.bounds.midX, y: v.bounds.minY, width: 0, height: 0)
//popVC.preferredContentSize = popVC.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
//popVC.preferredContentSize = CGSize(width: 220, height: 140)
present(popVC, animated: true)
popup = popVC
}
If I explicitly set the preferredContentSize to 220x140 then the popover is displayed with that size, like this:
Image showing explicitly set popover size
If I set the preferredContentSize to systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) then the popover does not set its size to the content (what is desired) but the popover is sized to the system default, like this:
Image showing system default popover size

Nevermind, I found a workaround. Since the popup only consists of a linear list of buttons I can calculate the width of the text for the widest button and then set the popup size based upon that width.

Related

Why can't I get the height between the nav bar and the top of the keyboard correctly?

I have a weird problem. I'm trying to calculate the exact height between the bottom of the navigation bar and the top of the keyboard no matter which iOS device I'm running the app on. Here is the method where I'm doing this calculation:
#objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
let navigationBarHeight: CGFloat = self.navigationController!.navigationBar.frame.height
let viewableArea = screenSize.height - keyboardRectangle.height - reportPostInstructionContainerViewHeight - characterCountContainerViewHeight - reportPostInstructionContainerViewHeight + 4
//iPhone 12 and above is "- 20"; iPhone 8 needs to be "+ 4"; iPhone 12 mini is "- 24"
print("**** navigationBarHeight: \(navigationBarHeight)")
print("**** keyboardHeight: \(keyboardHeight)")
print("**** screenSize.height: \(screenSize.height)")
print("**** total screen height - keyboard height: \(screenSize.height - keyboardRectangle.height)")
print("**** viewableArea: \(viewableArea)")
textViewHeight = viewableArea
print("**** textViewHeight: \(textViewHeight)")
UIView.animate(withDuration: 0.01, animations: { () -> Void in
self.textView.anchor(
top: self.horizontalDividerView.bottomAnchor,
leading: self.view.leadingAnchor,
bottom: nil,
trailing: self.view.trailingAnchor,
identifier: "ReportPostPFAVC.textView.directionalAnchors",
size: .init(width: 0, height: self.textViewHeight)
)
self.textView.layoutIfNeeded()
})
}
}
The line where "viewableArea" is being calculated seems to be the issue. For example, if I'm running the app on an iPhone 8, I need to add 4 to this calculation in order to size the text view properly.
Here is an image for reference:
I'm trying to get the bar with the "Report" button to sit perfectly on top of the keyboard. But, if I test on different devices sometimes I need to subtract 20 or 24 instead of adding 4.
I don't really understand where this gap is coming from.
Can anyone advise?
I will try to approach this from a different angle as I am not sure where exactly your issue is and what exactly was your logic from the code alone that you provided.
What you need to achieve is to find the coordinates of two frames in the same coordinate system. The two frames being; the keyboard frame and the navigation bar frame. And the "same coordinate system" is best defined by one of your views such as the view of your view controller.
There are convert methods on UIView which are designed to convert frames to/from different coordinate systems such as views.
So in your case all you need to do is
let targetView = self.view!
let convertedNavigationBarFrame = targetView.convert(self.navigationController!.navigationBar.bounds, from: self.navigationController!.navigationBar)
let convertedKeyboardFrame = targetView.convert(keyboardFrame.cgRectValue, from: nil)
In this example I used self.view as my coordinate system in which I want the two frames. This means the coordinates will be within a view controller. To get a height between two frames (which is your question) I could use absolutely any view that is in same window hierarchy and I should be getting the same result.
Then in this example I convert bounds of navigation bar from navigation bar, to target view. I found this to be best approach when dealing with UIView frames.
Last I convert keyboard frame to target view. The keyboard frame has a screen coordinate system which leads to using from: nil.
Getting the vertical distance between them is then a simple subtraction of two vertical coordinates
convertedKeyboardFrame.minY - convertedNavigationBarFrame.maxY
To have a full example I cerated a new project. In storyboard:
I added a navigation controller
I set the navigation controller to be "initial".
I set the old auto-generated view controller to be the root view controller of the navigation controller.
I added a text field which will trigger the
appearance of keyboard.
Then applied the following code:
The example code:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
self.navigationController?.navigationBar.backgroundColor = .green
}
private lazy var checkView: UIView = {
let checkView = UIView(frame: .zero)
checkView.backgroundColor = UIColor.black
checkView.layer.borderColor = UIColor.red.cgColor
checkView.layer.borderWidth = 5
self.view.addSubview(checkView)
return checkView
}()
#objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let targetView = self.view!
let convertedNavigationBarFrame = targetView.convert(self.navigationController!.navigationBar.bounds, from: self.navigationController!.navigationBar)
let convertedKeyboardFrame = targetView.convert(keyboardFrame.cgRectValue, from: nil)
checkView.frame = CGRect(x: 30.0, y: convertedNavigationBarFrame.maxY, width: 100.0, height: convertedKeyboardFrame.minY - convertedNavigationBarFrame.maxY)
}
}
}
The checkView appears between navigation bar and keyboard to show that the computation is correct. The view should fill the space between the two items and border is used to show that this view does not stretch below keyboard or above navigation bar.

systemLayoutSizeFitting not returning proper size of view

I'm setting a view controller's view as my UITableView's header.
var headerView = CommunityPostDetailTableHeaderViewController()
override func viewDidLoad() {
// other stuffs
headerView.view.frame = CGRect(x: 0, y: 0, width: self.tableView.frame.size.width, height: 100)
headerView.delegate = self
self.tableView.tableHeaderView = headerView.view
}
And using this bit of code to resize it according to the size of view.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let headerView = tableView.tableHeaderView {
let height = headerView.systemLayoutSizeFitting(CGSize(width: tableView.bounds.width, height: 0)).height
var headerFrame = headerView.frame
// Comparison necessary to avoid infinite loop
if height != headerFrame.size.height {
headerFrame.size.height = height
headerView.frame = headerFrame
tableView.tableHeaderView = headerView
}
}
}
I'm using this technique for two of my table views.
tableview is presented directly from a view controller like so :
let playerController = VideoDetailController()
playerController.modalPresentationStyle = .fullScreen
playerController.video = video
self.present(playerController, animated: true, completion: nil)
Working fine in all iOS devices.
the other is presented by embedding inside a navigation controller :
let communityPostDetailVC = CommunityPostDetailViewController()
communityPostDetailVC.delegate = self
if let indexpath = indexpath {
communityPostDetailVC.communityPost = datasource[indexpath.row]
communityPostDetailVC.indexpath = indexpath
}
let navigationController = UINavigationController(rootViewController: communityPostDetailVC)
navigationController.modalPresentationStyle = .fullScreen
self.present(navigationController, animated: true)
Not resizing properly on iPhone 5/5s/SE(1st gen)/6/6s/7/8/SE(2nd gen).
I can't figure out why it is not working on smaller phones. You can see blank space in the comparison attachment below. In smaller SE the space is even more.
Any suggestions/ideas are welcome. I'm clueless at this point.
PS: I've nested view controller's. the headerView is a view controller holding another view controller's view. The FB logo and the pink label underneath is part of the nested view controller. Other than that everything else is in headerView's view controller's view.
you are using fullscreen , but you image is a fixed size image . pink colour view also fixed size.
maybe its making a problem .
its actually not an answer. a suggestion for you. you may review again your code. hope you will find .
happy coding

frame of navigationitem titleView in viewcontroller view

How do I get the frame of a navigationItem's titleView in the coordinate system of the viewcontroller's view?
if let navBarHeight = navigationController?.navigationBar.frame.height,
let navBarWidth = navigationController?.navigationBar.frame.width {
myCustomTitleView.frame = CGRect(x: 0, y: 0, width: navBarWidth, height: navBarHeight)
navigationItem.titleView = myCustomTitleView
}
However, when I check myCustomTitleView's frame origin, I get (0, 0).
I then tried to translate this origin to the viewcontroller's view. what I got was (0,-44), which accounts for the navigation bar height but not for the x-offset.
let originInVCView = view.convert(myCustomTitleView.frame.origin, from: myCustomTitleView)
This can't be right as the titleView obviously has an offset (space for the back button).
How do I correctly extract the translated titleView origin?
You want to make sure you have set the navigation item in viewDidLoad() first. Otherwise it will be nil.
override func viewDidLoad() {
super.viewDidLoad()
let imageView = UIImageView(image: UIImage(named: "MY_IMAGE"))
navigationItem.titleView = imageView
When done you can get the frame in the VC's viewDidAppear where the view has been laid out:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let nItemFrame = navigationItem.titleView?.frame //<<<---
}

Showing a popover partially off screen

I am trying to recreate the effect that YouTube mobile app on iOS has, when a video is playing in full-screen, landscape mode. Just the top part of a window (next/recommended videos) is visible, and swiping up shows them overlapped with video (that keeps running in background). Swiping down hides them again.
So I added the following code within my video controller that shows the video in landscape mode:
private func showPopover() {
let popoverController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "popoverController")
popoverController.modalPresentationStyle = .popover
popoverController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
popoverController.popoverPresentationController?.delegate = self
let desiredWidth:CGFloat = 200
let desiredHeight:CGFloat = 300
// Initially it's at low right corner
let origin = CGPoint(x: (self.view.superview?.frame.width)! - desiredWidth - 10, y: (self.view.superview?.frame.height)! - 10)
let size = CGSize(width: desiredWidth, height: desiredHeight)
popoverController.popoverPresentationController?.sourceRect = CGRect(origin: origin, size: size)
popoverController.popoverPresentationController?.sourceView = popoverController.view
self.present(popoverController, animated: true, completion: nil)
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
The popover shows up fine, but is always completely on screen. I need it to be only partially visible (just the top 10 pixels, if possible). But something is preventing the view controller from being positioned that way.
How do I move the popover so that only top 10 pixels are visible? It should look something like in the image below (note: this was generating by editing image, not programmatically):
Replace this:
popoverController.popoverPresentationController?.sourceView = popoverController.view
with this:
popoverController.popoverPresentationController?.sourceView = self.view
UPDATE
Ok I understand now. My answer would be it is not possible to only show some part of the popover.
My suggestion here is using UITableViewController as your popover and making its height just enough to show the first row, which is the text in this case.
Then, in optional func scrollViewDidScroll(_ scrollView: UIScrollView) delegate method, implement logics to detect swiping up to show and swiping down to hide.
Found a way, and it's ridiculously easy (with hindsight, of course!). Changed the last line of showPopover function to:
self.present(popoverController, animated: true) {
popoverController.popoverPresentationController?.presentedView?.center = CGPoint(x: (self.view.superview?.frame.width)! - desiredWidth - 10, y: (self.view.superview?.frame.height)! + 100)
}
I will get a specific number instead of using 100 but the view moves to this position without any issues.

iOS - Custom presentation controller doesn't update the presented navigationBar height appropriately for split view

I have created my own subclass of UIPresentationController and I am presenting a navigation controller using it. The purpose is to somewhat mimic the behavior of UIPopoverPresentationController but allow for more customization.
So the problem I am experiencing is that on iPad when the user adjusts the size of the app using splitview, the navigation bar's height doesn't update correctly.
When the view is in a popover style it is supposed to use a height of 44 for the nav bar and when it is in fullscreen style it uses a height of 64. This is happening correctly upon first presenting the controller. However if the user adjusts the app using splitview the nav bar height does not update at all.
In my UIPresentationController subclass I am doing the following:
I set the frame based on the container view's width:
override func frameOfPresentedViewInContainerView() -> CGRect {
if let containerView = containerView {
if containerView.bounds.width > 500 {
let preferredSize = presentedViewController.preferredContentSize
return CGRect(x: containerView.bounds.width - preferredSize.width - 20, y: 16, width: preferredSize.width, height: preferredSize.height)
} else {
return containerView.bounds
}
} else {
return CGRectZero
}
}
Then I update the frame whenever I get the willLayoutSubviews call:
override func containerViewWillLayoutSubviews() {
presentedViewController.view.frame = frameOfPresentedViewInContainerView()
}
When I examine the presentedViewController's view, it is getting all the correct values and visually is the right size. The only problem is that the nav bar will remain the height that it was originally presented at (whether that is 44 or 64) and will either leave a gap or extend passed its bounds.
It appears I found a solution that works. Inside my containerViewWillLayoutSubviews function I simply access the navigation controller's navigationBar property and manually set its frame correctly.
if let navigationController = presentedViewController as? UINavigationController {
navigationController.navigationBar.frame = CGRect(x: 0,
y: 0,
width: presentedViewController.view.frame.size.width,
height: containerView.bounds.width > 500 ? 44 : 64)
}
This does seem a bit fragile though, but its working fine for me.

Resources