Updating variable in view constraint (SnapKit) - swift

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.

Related

Change content size UIScrollView dynamically

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.

How to make scrollview scroll separately to its content height in swift

I have view hierarchy like below in storyboard
here for content main constrains top = 0, leading = 0, trailing = 0, bottom = 0
here for scrollview constrains top = 0, leading = 0, trailing = 0, bottom = 0
here for View constrains top = 0, leading = 0, trailing = 0, bottom = 0
for ContentView constrains top = 0, leading = 0, trailing = 0
for TblReview constrains top = 0, leading = 0, trailing = 0, bottom = 20
for Productcollectionview constrains top = 0, leading = 0, trailing = 0, bottom = 20, height = 400
and i have Productcollectionview height outlet like below in swift file
and i don't want collectionview separate scrolling.. i want total view to scroll according to collectionview cells.. so for that i have written below code but with this code contentView and tblReview also scrolling upto productioncollectionview height i need contentView should scroll upto its content height and tblReview should scroll upto its rows
how to make scrolling separately to its height.
please help me to solve this issue
#IBOutlet weak var productCollHeight: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
productCollectionView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
let collectionView = object as? UICollectionView
if collectionView == self.productCollectionView{
if(keyPath == "contentSize"){
if let newvalue = change?[.newKey]
{
let newsize = newvalue as! CGSize
self.productCollHeight.constant = newsize.height
}
}
}
}
#IBAction func aboutCompany(_ sender: UIButton){
self.productCollectionView.isHidden = true
self.tblReview.isHidden = true
self.contentView.isHidden = false
}
#IBAction func review(_ sender: UIButton){
self.productCollectionView.isHidden = true
self.tblReview.isHidden = false
self.contentView.isHidden = true
}
#IBAction func productOfSeller(_ sender: UIButton){
self.productCollectionView.isHidden = false
self.tblReview.isHidden = true
self.contentView.isHidden = true
}
and i am hiding and showing tblReview and contentView according to need
with the above code tblReview and contentView are also scrolling upto productCollectionView height.. please help me to solve this error
EDIT: share this seller is out of scrollview so here contentView height is not not so long but if i scroll the contentView also scrolling too long like productioncollectionview
this is contentView which is scrolling too long like productioncollectionview
if your all constraint are proper than just use it like this you don't need to count every time. UICollectionView has intrinsicContentSize it will count it properly.
final class ContentSizedCollectionView: UICollectionView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
put that code in controller or wherever you wan to put and assign ContentSizedCollectionView class to your collectionview.
NOTE: you can also use it with UITableView after creating for UITableView.

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.

How to fix the wrong frame after device rotation?

I use custom alert view in my application. When called, its frame completely fills the screen. However, after the device rotates, it does not look right.
Then I try to change my alert frame and assign it a new screen size in viewWillTransition
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: nil) { _ in UIView.setAnimationsEnabled(true) }
UIView.setAnimationsEnabled(false)
super.viewWillTransition(to: size, with: coordinator)
if let _ = alert {
alert?.rotateAlert(to: size)
Logger.Log("after size = \(alert?.backgroundView.frame.size)")
Logger.Log("after frame = \(alert?.frame)")
Logger.Log("after bounds = \(alert?.bounds)")
}
I tried to do the same in viewDidLayoutSubviews
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let _ = alert {
Logger.Log("viewDidLayoutSubviews before rotation")
alert?.rotateAlert(to: self.view.frame.size)
Logger.Log("after size = \(alert?.backgroundView.frame.size)")
Logger.Log("after frame = \(alert?.frame)")
Logger.Log("after bounds = \(alert?.bounds)")
}
}
func rotateAlert(to size: CGSize) {
self.frame.size = size
backgroundView.frame = frame
}
Thus the frame of my alert changes. But its size is still smaller than the screen in height, thus forming an empty area.
Why is this happening? And how do I fix this? (I use Swift4)
To place your alert in the center of the screen, assuming that it is an instance of UIView, do this:
alert.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
alert.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
// Or
NSLayoutConstraint.activate([
alert.centerXAnchor.constraint(equalTo: view.centerXAnchor),
alert.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
This is using auto layout to position your views, and the views will properly update when the trait collection changes.
When adding a view programmatically the view doesn't adjust according to the screen rotation because you have fixed the view height, width so after rotation you need to again give its frame.
This doesn't happen when you use constraints to add a view either programmatically or from the storyboard
For your alert to resize you have to again give its frame in viewDidLayoutSubviews()
override func viewDidLayoutSubviews() {
YourAlertView.frame = CGRect(x:0 , y: 0,width: self.view.frame.width, height: self.view.frame.height )
}
Works like a charm for me.

Change height and width of view according to scrollview scroll horizontally using auto layout

Container View hierarchy:-
Container View
Scrollview
view left
view center
view right
FloatingBottomView
I need to change height and width constrain of FloatingBottomView according to scrollview scroll horizontally.
Initial Outlet:-
#IBOutlet weak var constantFloatingBottomViewWidth: NSLayoutConstraint! // 300
#IBOutlet weak var constantFloatingBottomViewHeight: NSLayoutConstraint! // 70
override func viewWillAppear(_ animated: Bool) {
self.scrollView.contentOffset.x = self.view.bounds.width
// view center in scrollview
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x < self.view.bounds.width {
// when scrollview move view center to view left, constantFloatingBottomViewWidth and height goes to down at some point
}
else if scrollView.contentOffset.x > self.view.bounds.width {
// when scrollview move view left to view center, constantFloatingBottomViewWidth & height goes up to same point range while it down and back to original constantFloatingBottomViewWidth and height
}
}
i have tried using this way to get some scale in scrollViewDidScroll method.
scroll quick left to right or vice-versa did not get exactly out-put
let scale = abs(scrollView.contentOffset.x / self.view.bounds.width)
print("scale:=\(scale)")
I think you need to give button animation like this
For this you have nothing to do just add animation to button like follow
call setupButtonanimation in viewDidLoad
func setupButtonAnimation()
{
let animation = CABasicAnimation.init(keyPath: "transform.scale")
animation.fromValue = 1.0
animation.toValue = 0.45
animation.duration = 1.0
//Set the speed of the layer to 0 so it doesn't animate until we tell it to
self.btn1.layer.speed = 0.0;
self.btn1.layer.add(animation, forKey: "transform");
}
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
let screenSize = self.view.bounds.size
if let btn = self.btn1
{
var factor:CGFloat = 1.0
factor = scrollView.contentOffset.x / screenSize.width
if factor > 1
{
factor = 2 - factor
}
print(factor)
//This will change the size
let timeOffset = CFTimeInterval(1-factor)
btn.layer.timeOffset = timeOffset
}
}