Change content size UIScrollView dynamically - swift

I have a scroll view, when the view is first loaded, the size is set dynamically, but when I click on the button, the internal size of my elements changes and I need to change the internal size of the scroll, but it does not change. Someone knows how to fix it?
DispatchQueue.main.async {
var contentRect = CGRect()
for view in self.scrollView.subviews {
contentRect = contentRect.union(view.frame)
self.scrollView.contentSize = contentRect.size
}
}

If you really don't want to use auto-layout / constraints, you can call this function each time you add (or remove) a subview from the scroll view, or after you've changed the size(s) of the subview(s):
func updateContentSize() -> Void {
// this will get the right-edge of the right-most subview
let width = scrollView.subviews.map {$0.frame.maxX}.max() ?? 0.0
// this will get the bottom-edge of the bottom-most subview
let height = scrollView.subviews.map {$0.frame.maxY}.max() ?? 0.0
// set the contentSize
scrollView.contentSize = CGSize(width: width, height: height)
}

This solution is for auto-layout/constraints.
You need a reference constraint to manipulate the height of the inner container view of the scrollview.
private var _constraintInnerContainerScroll:NSLayoutConstraint?
You need to set the initial height of the inner container view, suppose 700.0
private let _containerViewHeightFixed : CGFloat = 700.0
then you need to save the reference
_constraintInnerContainerScroll = _containerView.heightAnchor.constraint(equalToConstant: _containerViewHeightFixed)
_constraintInnerContainerScroll?.isActive = true
You initial view is setup and ready, now suppose you add 2 more subview of height 100.0 each, now your new inner container view height should be 700.0+200.0 = 900.0
if let const1 = _constraintInnerContainerScroll{
const1.constant = _containerViewHeightFixed + 200.0
UIView.animate(withDuration: 0.5) {
self?._containerView.layoutIfNeeded()
}else{
print("constraint reference not saved")
}
let me know if this works for you, or if this can be improved.

Related

Updating variable in view constraint (SnapKit)

View is initialized with following constraints
View.snp.makeConstraints { (para) in
View.topConstraint = para.top.equalTo(parentview.snp.top).constraint
View.LeadingConstraint = para.leading.equalTo(parentview.snp.leading).constraint
View.TrailingConstraint = para.trailing.equalTo(parentview.snp.trailing).constraint
View.BottomConstraint =para.bottom.equalTo(parentview.snp.bottom).offset(-getheight).constraint
}
where getheight = parentview.frame.size.height/2 ;
when parentview changes its dimensions.View doesnt update its height as constraints are not called again.
any way to update or recall its constraints other the remakingConstraint which is not feasible at large scale.
Have tried:
View.updateConstraints()
View.setNeedsUpdateConstraints()
View.setNeedsLayout()
I need reference to each constraints because
if View.bottomTouch {
View.bottomConstraint.update(offset: View. BottomConstraint.layoutConstraints[0].constant + CurrentPoint - PreviousPoint)
}
Is there a reason you don't want to use 50% of the parent view height?
View.snp.makeConstraints { (para) in
para.top.equalTo(parentview.snp.top)
para.leading.equalTo(parentview.snp.leading)
para.trailing.equalTo(parentview.snp.trailing)
// 50% of the parent view height
para.height.equalTo(parentview.snp.height).multipliedBy(0.5)
// instead of this
//para.bottom.equalTo(parentview.snp.bottom).offset(-getheight)
}
Edit - after comments...
Keeping a reference to a constraint for the purposes of dragging a view is a very different question from "Keep the child view at 50% of the height of the parent view."
Give this a try...
It will create a cyan "parentView" with a blue "childView" (subview). Dragging the blue view (Pan Gesture) will drag its bottom up / down. Tapping anywhere (Tap Gesture) will toggle the insets on the frame of the parentView between 20 and 60.
When the parentView frame changes - either from the tap or, for example, on device rotation - the "childView" bottom will be reset to 50% of the height of the "parentView":
class ViewController: UIViewController {
let parentView = UIView()
let childView = UIView()
// childView bottom constraint
var bc: Constraint!
override func viewDidLoad() {
super.viewDidLoad()
parentView.backgroundColor = .cyan
childView.backgroundColor = .blue
parentView.addSubview(childView)
view.addSubview(parentView)
parentView.snp.makeConstraints { para in
para.top.leading.trailing.bottom.equalTo(self.view.safeAreaLayoutGuide).inset(20.0)
}
// childView's bottom constraint offset will be set in viewDidLayoutSubviews()
childView.snp.makeConstraints { para in
para.top.leading.trailing.equalToSuperview()
bc = para.bottom.equalToSuperview().constraint
}
let p = UIPanGestureRecognizer(target: self, action: #selector(panHandler(_:)))
childView.addGestureRecognizer(p)
let t = UITapGestureRecognizer(target: self, action: #selector(tapHandler(_:)))
view.addGestureRecognizer(t)
}
#objc func tapHandler(_ g: UITapGestureRecognizer) -> Void {
// on tap, toggle parentView inset
// between 20 and 60
// this will trigger viewDidLayoutSubviews(), where the childView bottom
// constraint will be reset to 50% of the parentView height
var i: CGFloat = 60.0
if parentView.frame.origin.x > 20 {
i = 20.0
}
parentView.snp.updateConstraints { para in
para.top.leading.trailing.bottom.equalTo(self.view.safeAreaLayoutGuide).inset(i)
}
}
#objc func panHandler(_ g: UIPanGestureRecognizer) -> Void {
let translation = g.translation(in: g.view)
// update bottom constraint constant
bc.layoutConstraints[0].constant += translation.y
// reset gesture translation
g.setTranslation(CGPoint.zero, in: self.view)
}
var parentViewHeight: CGFloat = 0.0
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// reset childView's bottom constraint
// to 50% of its superView's height
// ONLY if parentView frame height has changed
if parentView.frame.height != parentViewHeight {
parentViewHeight = parentView.frame.height
bc.layoutConstraints[0].constant = -parentViewHeight * 0.5
}
}
}
Firstly, check whether the getheight value did update when the parent view layout change. In order to reload the existing constraints, you may need to call layoutIfNeeded() of your parent view.

Identifying Objects in Firebase PreBuilt UI in Swift

FirebaseUI has a nice pre-buit UI for Swift. I'm trying to position an image view above the login buttons on the bottom. In the example below, the imageView is the "Hackathon" logo. Any logo should be able to show in this, if it's called "logo", since this shows the image as aspectFit.
According to the Firebase docs page:
https://firebase.google.com/docs/auth/ios/firebaseui
You can customize the signin screen with this function:
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
return FUICustomAuthPickerViewController(nibName: "FUICustomAuthPickerViewController",
bundle: Bundle.main,
authUI: authUI)
}
Using this code & poking around with subviews in the debuggers, I've been able to identify and color code views in the image below. Unfortunately, I don't think that the "true" size of these subview frames is set until the view controller presents, so trying to access the frame size inside these functions won't give me dimensions that I can use for creating a new imageView to hold a log. Plus accessing the views with hard-coded index values like I've done below, seems like a pretty bad idea, esp. given that Google has already changed the Pre-Built UI once, adding a scroll view & breaking the code of anyone who set the pre-built UI's background color.
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
// Create an instance of the FirebaseAuth login view controller
let loginViewController = FUIAuthPickerViewController(authUI: authUI)
// Set background color to white
loginViewController.view.backgroundColor = UIColor.white
loginViewController.view.subviews[0].backgroundColor = UIColor.blue
loginViewController.view.subviews[0].subviews[0].backgroundColor = UIColor.red
loginViewController.view.subviews[0].subviews[0].tag = 999
return loginViewController
}
I did get this to work by adding a tag (999), then in the completion handler when presenting the loginViewController I hunt down tag 999 and call a function to add an imageView with a logo:
present(loginViewController, animated: true) {
if let foundView = loginViewController.view.viewWithTag(999) {
let height = foundView.frame.height
print("FOUND HEIGHT: \(height)")
self.addLogo(loginViewController: loginViewController, height: height)
}
}
func addLogo(loginViewController: UINavigationController, height: CGFloat) {
let logoFrame = CGRect(x: 0 + logoInsets, y: self.view.safeAreaInsets.top + logoInsets, width: loginViewController.view.frame.width - (logoInsets * 2), height: self.view.frame.height - height - (logoInsets * 2))
// Create the UIImageView using the frame created above & add the "logo" image
let logoImageView = UIImageView(frame: logoFrame)
logoImageView.image = UIImage(named: "logo")
logoImageView.contentMode = .scaleAspectFit // Set imageView to Aspect Fit
// loginViewController.view.addSubview(logoImageView) // Add ImageView to the login controller's main view
loginViewController.view.addSubview(logoImageView)
}
But again, this doesn't seem safe. Is there a "safe" way to deconstruct this UI to identify the size of this button box at the bottom of the view controller (this size will vary if there are multiple login methods supported, such as Facebook, Apple, E-mail)? If I can do that in a way that avoids the hard-coding approach, above, then I think I can reliably use the dimensions of this button box to determine how much space is left in the rest of the view controller when adding an appropriately sized ImageView. Thanks!
John
This should address the issue - allowing a logo to be reliably placed above the prebuilt UI login buttons buttons + avoiding hard-coding the index values or subview locations. It should also allow for properly setting background color (also complicated when Firebase added the scroll view + login button subview).
To use: Create a subclass of FUIAuthDelegate to hold a custom view controller for the prebuilt Firebase UI.
The code will show the logo at full screen behind the buttons if there isn't a scroll view or if the class's private constant fullScreenLogo is set to false.
If both of these conditions aren't meant, the logo will show inset taking into account the class's private logoInsets constant and the safeAreaInsets. The scrollView views are set to clear so that a background image can be set, as well via the private let backgroundColor.
Call it in any signIn function you might have, after setting authUI.providers. Call would be something like this:
let loginViewController = CustomLoginScreen(authUI: authUI!)
let loginNavigationController = UINavigationController(rootViewController: loginViewController)
loginNavigationController.modalPresentationStyle = .fullScreen
present(loginNavigationController, animated: true, completion: nil)
And here's one version of the subclass:
class CustomLoginScreen: FUIAuthPickerViewController {
private var fullScreenLogo = false // false if you want logo just above login buttons
private var viewContainsButton = false
private var buttonViewHeight: CGFloat = 0.0
private let logoInsets: CGFloat = 16
private let backgroundColor = UIColor.white
private var scrollView: UIScrollView?
private var viewContainingButton: UIView?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// set color of scrollView and Button view inside scrollView to clear in viewWillAppear to avoid a "color flash" when the pre-built login UI first appears
self.view.backgroundColor = UIColor.white
guard let foundScrollView = returnScrollView() else {
print("😡 Couldn't get a scrollView.")
return
}
scrollView = foundScrollView
scrollView!.backgroundColor = UIColor.clear
guard let foundViewContainingButton = returnButtonView() else {
print("😡 No views in the scrollView contain buttons.")
return
}
viewContainingButton = foundViewContainingButton
viewContainingButton!.backgroundColor = UIColor.clear
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Create the UIImageView at full screen, considering logoInsets + safeAreaInsets
let x = logoInsets
let y = view.safeAreaInsets.top + logoInsets
let width = view.frame.width - (logoInsets * 2)
let height = view.frame.height - (view.safeAreaInsets.top + view.safeAreaInsets.bottom + (logoInsets * 2))
var frame = CGRect(x: x, y: y, width: width, height: height)
let logoImageView = UIImageView(frame: frame)
logoImageView.image = UIImage(named: "logo")
logoImageView.contentMode = .scaleAspectFit // Set imageView to Aspect Fit
logoImageView.alpha = 0.0
// Only proceed with customizing the pre-built UI if you found a scrollView or you don't want a full-screen logo.
guard scrollView != nil && !fullScreenLogo else {
print("No scrollView found.")
UIView.animate(withDuration: 0.25, animations: {logoImageView.alpha = 1.0})
self.view.addSubview(logoImageView)
self.view.sendSubviewToBack(logoImageView) // otherwise logo is on top of buttons
return
}
// update the logoImageView's frame height to subtract the height of the subview containing buttons. This way the buttons won't be on top of the logoImageView
frame = CGRect(x: x, y: y, width: width, height: height - (viewContainingButton?.frame.height ?? 0.0))
logoImageView.frame = frame
self.view.addSubview(logoImageView)
UIView.animate(withDuration: 0.25, animations: {logoImageView.alpha = 1.0})
}
private func returnScrollView() -> UIScrollView? {
var scrollViewToReturn: UIScrollView?
if self.view.subviews.count > 0 {
for subview in self.view.subviews {
if subview is UIScrollView {
scrollViewToReturn = subview as? UIScrollView
}
}
}
return scrollViewToReturn
}
private func returnButtonView() -> UIView? {
var viewContainingButton: UIView?
for view in scrollView!.subviews {
viewHasButton(view)
if viewContainsButton {
viewContainingButton = view
break
}
}
return viewContainingButton
}
private func viewHasButton(_ view: UIView) {
if view is UIButton {
viewContainsButton = true
} else if view.subviews.count > 0 {
view.subviews.forEach({viewHasButton($0)})
}
}
}
Hope this helps any who have been frustrated trying to configure the Firebase pre-built UI in Swift.

Resizing NSWindow don't autoresize its contentView correctly

I have an NSWindow with it's contentView. In the awakeFromNib() of the NSWindow I have the following code:
override func awakeFromNib()
{
super.awakeFromNib()
/// Customize Window through XIBs
self.title = "Main Window"
let screenFrame = NSScreen.main?.frame
let windowPercentage: CGFloat = 0.9;
let offset: CGFloat = (1.0 - windowPercentage) / 2.0;
let windowFrame: NSRect = NSRect(x: (screenFrame?.width)! * offset, y: (screenFrame?.height)! * offset, width: (screenFrame?.width)! * windowPercentage, height: (screenFrame?.height)! * windowPercentage )
self.setFrame(windowFrame,display: true,animate: true)
self.backgroundColor = NSColor.lightGray
self.isRestorable = true
// Customize contentView
let viewPercentage: CGFloat = 0.6
self.contentView?.setFrameSize(NSSize(width: self.frame.size.width * viewPercentage, height: self.frame.size.height))
self.contentView?.setFrameOrigin( NSMakePoint( ( (self.frame.width) - (self.contentView?.frame.width)! )/2, ( (self.frame.height) - (self.contentView?.frame.height)!)/2) )
self.contentView?.autoresizingMask = [.width, .height, .minXMargin,.maxXMargin,.maxYMargin,.minYMargin]
}
I am trying to set up the contentView in the center and with a percentage of its NSWindow frame but it's failing when i resize the window. As soon as I start to resize the Window the contentView it's not resizing correctly, as you can see from the following image(the second one):
Image one
Image two
Should I override the resize(withOldSuperviewSize:) method to achieve this? (also the autoresize from interface builder don't resolve the issue)
You shouldn't attempt to change the size of the content view like that. I don't believe it's supported. The window controls the content view's size.
If you want a view of your own to occupy only a portion of the window's content area, you should add your view as a subview of the content view.

Window animates width but not height

I have a preferences window with a NSTabViewController hooked up to the toolbar for selecting tabs. I want the window to be resizable, and to resize if necessary when switching tabs to fit the new tab's size.
I'm subclassing NSTabViewController with the following overload:
override var selectedTabViewItemIndex: Int
{
didSet
{
guard let view = tabViewItems[selectedTabViewItemIndex].view,
let window = view.window
else { return }
let minSize = view.fittingSize
let contentRect = NSWindow.contentRect(forFrameRect: window.frame,
styleMask: window.styleMask)
let minRect = NSRect(origin: contentRect.origin, size: minSize)
let newRect = minRect.union(contentRect)
let newFrame = NSWindow.frameRect(forContentRect: newRect,
styleMask: window.styleMask)
window.animator().setFrame(newFrame, display: true, animate: true)
}
}
The result is that it animates resizing horizontally, and at the end it suddenly resizes vertically as well. How do I get it to just animate both directions at once?
Do you have height constraints on any of the tabs? Those might prevent the window from getting larger (even while offscreen).

NSTextView not resizing properly after setFrameSize

In an NSTextView subclass I have created, I want to resize the height of the view to the height of the text within it. To execute this, I used apple's recommended procedure of counting lines within a text view:
private func countln() -> Int {
var nlines: Int
var index: Int
var range = NSRange()
let nGlyphs = lManager.numberOfGlyphs
for (nlines = 0, index = 0; index < nGlyphs; nlines++) {
lManager.lineFragmentRectForGlyphAtIndex(index, effectiveRange: &range)
index = NSMaxRange(range);
}
return nlines
}
This method works as expected and returns the correct number of lines in the text view. The issue lies in the resizing of the view, which I inserted into the delegate method that is called on text change:
func textDidChange(notification: NSNotification) {
let newHeight = CGFloat(28 * countln())
let ogHeight = self.frame.height
self.setFrameSize(NSSize(width: self.frame.width, height: newHeight))
self.setFrameOrigin(NSPoint(x: self.frame.origin.x, y: (self.frame.origin.y - self.frame.height) + ogHeight))
Swift.print(frame.height)
}
The setFrameSize variable function resizes the height of the view based not the number of lines in the view (multiplied by a constant that is more-or-less the height of each line of text). Everything works perfectly until immediately after the change of height is made, when the text view's height changes to an unanticipated incorrect height. I presume there is an issue with the frequent redrawing of the view in relation to the way I am resizing it. Any help on how to solve this issue of incorrect height resizing is greatly appreciated.
Thanks in advance.