Note: I'm pretty new working with iOS UI.
I want to create a custom view that stacks a custom view inside.
So I created the custom UIStackView
class CustomStackView: UIStackView {
func addItem(color:UIColor){
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "RowView", bundle: bundle)
let rowView = RowView();
let view = nib.instantiate(withOwner: rowView, options: nil).first as! UIView
rowView.addSubview(view)
rowView.view.backgroundColor = color;
addArrangedSubview(rowView)
}
}
class RowView :UIView{
#IBOutlet var view: UIView!
override public var intrinsicContentSize: CGSize {
return CGSize(width: view.frame.width,height:view.frame.height)
}
}
in the RowView.xib I created a simple layout for testing:
Simulated Metrics = Freeform
Height = 100
And the ViewController.swift:
class ViewController: UIViewController {
#IBOutlet weak var customStackView: CustomStackView!
#IBOutlet weak var constraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
customStackView.addItem(color: UIColor.red)
customStackView.addItem(color: UIColor.blue)
customStackView.addItem(color: UIColor.green)
}
#IBAction func click(_ sender: Any) {
constraint.constant = -customStackView.frame.height
UIView.animate(withDuration: 4, animations: {
self.view.layoutIfNeeded();
},completion:nil)
}
}
The result:
The first and second item are displayed correctly but the third is higher than expected.
In addition if I click the button (which should hide the Stackview) keep the "extra" height visible:
How can I fix that?
Edit: Tried the #KristijanDelivuk solution adding a trailing view. And didn't work. Adding cyan color to the view I got this result:
override func viewDidLoad() {
super.viewDidLoad()
customStackView.addItem(color: UIColor.red)
customStackView.addItem(color: UIColor.blue)
customStackView.addItem(color: UIColor.green)
let view = UIView();
view.heightAnchor.constraint(equalToConstant: 50).isActive = true;
view.backgroundColor = UIColor.cyan;
customStackView.addArrangedSubview(view)
}
You can try adding an empty UIView as your last element of UIStackView:
So your hierarchy should look something like this:
- STACKVIEW
-- 1ST ADDED CUSTOM VIEW
-- 2ND ADDED CUSTOM VIEW
-- 3RD ADDED CUSTOM VIEW
-- EMPTY UIVIEW
Empty UIView will take all unallocated space from 3rd view and all should be displayed correctly.
For repositioning button after hiding/showing stackview you can create for example "top constraint" and then on tap change top constraint height to (-) stackview.height or (+) stackview.height - This shouldn't be any problem.
Related
I notice that, if I perform add/ expand animation within an UIScrollView, it will cause unwanted scrolling behavior, when the UIScrollView fill with enough content to become scroll-able.
As you can see in the following animation, initially, the add/ expand animation works just fine.
When we have added enough item till the UIScrollView scrollable, whenever a new item is added, and UIScrollView will first perform scroll down, and then scroll up again!
My expectation is that, the UIScrollView should remain static, when add/ expand animation is performed.
Here's the code which performs add/ expand animation.
Add/ expand animation
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
// Clear off horizontal swipe in animation caused by addArrangedSubview
stackView.superview?.layoutIfNeeded()
customView.show()
// Perform expand animation.
UIView.animate(withDuration: 1) {
self.stackView.superview?.layoutIfNeeded()
}
}
Here's the constraint setup of the UIScrollView & added custom view item
Constraint setup
Custom view
class CustomView: UIView {
private var zeroHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.cornerRadius = stackView.frame.height / 2
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
zeroHeightConstraint = self.safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 0)
zeroHeightConstraint.isActive = false
}
func hide() {
zeroHeightConstraint.isActive = true
}
func show() {
zeroHeightConstraint.isActive = false
}
}
Here's the complete source code
https://github.com/yccheok/add-expand-animation-in-scroll-view
Do you have any idea why such problem occur, and we can fix such? Thanks.
Because of the way stack views arrange their subviews, animation can be problematic.
One approach that you may find works better is to embed the stack view in a "container" view.
That way, you can use the .isHidden property when adding an arranged subview, and allow the animation to update the "container" view:
The "add view" function now becomes (I added a Bool so we can skip the animation on the initial add in viewDidLoad()):
func addCustomView(_ animated: Bool) {
let customView = CustomView.instanceFromNib()
stackView.addArrangedSubview(customView)
customView.isHidden = true
if animated {
DispatchQueue.main.async {
UIView.animate(withDuration: 1) {
customView.isHidden = false
}
}
} else {
customView.isHidden = false
}
}
And we can get rid of all of the hide() / show() and zeroHeightConstraint in the custom view class:
class CustomView: UIView {
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
}
override func layoutSubviews() {
super.layoutSubviews()
borderView.layer.cornerRadius = borderView.bounds.height * 0.5
}
}
Since it's a bit difficult to clearly show everything here, I forked your project with the changes: https://github.com/DonMag/add-expand-animation-in-scroll-view
Edit
Another "quirk" of animating a stack view shows up when adding the first arranged subview (also, when removing the last one).
One way to get around that is to add an empty view as the first subview.
So, for this example, in viewDidLoad() before adding an instance of CustomView:
let v = UIView()
stackView.addArrangedSubview(v)
This will make the first arranged subview a zero-height view (so it won't be visible).
Then, if you're implementing removing custom views, just make sure you don't remove that first, empty view.
If your stack view has .spacing = 0 noting else is needed.
If your stack view has a non-zero spacing, add another line:
let v = UIView()
stackView.addArrangedSubview(v)
stackView.setCustomSpacing(0, after: v)
I did a little research on this and the consensus was to update the isHidden and alpha properties when inserting a view with animations.
In CustomView:
func hide() {
alpha = 0.0
isHidden = true
zeroHeightConstraint.isActive = true
}
func show() {
alpha = 1.0
isHidden = false
zeroHeightConstraint.isActive = false
}
In your view controller:
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
self.stackView.layoutIfNeeded()
UIView.animate(withDuration: 00.5) {
customView.show()
self.stackView.layoutIfNeeded()
}
}
Also, the constraints in your storyboard aren't totally correct. You are seeing a red constraint error because autolayout doesn't know the height of your stackView. You can give it a fake height and make sure that "Remove at build time" is checked.
Also, get rid of your scrollView contentView height constraint defined as View.height >= Frame Layout Guide.height. Autolayout doesn't need to know the height, it just needs to know how subviews inside of the contentView stack up to define its vertical content size.
Everything else looks pretty good.
I'm trying to set the origin and width/height of one UIView (red) to a second UIView (blue).
I am calling UIView.frame.origin or size and for some reason the y origin doesn't work.
I've also tried with layout constraints (see it commented out below), but this is overriding my blue fully constrained view.
Then I have a button that animates the red view to the side so you can see the blue view underneath, but I can't get them to line up to start with. Below is my code. In interface builder, I have both UIViews set up as containers. Blue is fully constrained with auto layout and red has no constraints.
import UIKit
class ViewController: UIViewController
{
#IBOutlet weak var blueContainer: UIView!
#IBOutlet weak var redContainer: UIView!
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
print(redContainer.frame)
redContainer.frame.origin.x = view.frame.width/2
redContainer.frame.size.width = view.frame.width
//try to line up y with origin and size
redContainer.frame.origin.y = blueContainer.frame.origin.y
redContainer.frame.size.height = blueContainer.frame.size.height
//also tried by using constraints
//redContainer.topAnchor.constraint(equalTo: blueContainer.topAnchor).isActive = true
//redContainer.heightAnchor.constraint(equalTo: blueContainer.heightAnchor).isActive = true
print(redContainer.frame)
}
#IBAction func slideRed(_ sender: Any) {
if redContainer.frame.origin.x == 0 {
UIView.animate(withDuration: 0.5) {
self.redContainer.frame.origin.x = self.view.frame.width/2
}
button.setTitle("Come Back Red!", for: .normal)
} else {
UIView.animate(withDuration: 0.5) {
self.redContainer.frame.origin.x = 0
}
button.setTitle("Go Away Red!", for: .normal)
}
}
}
ViewDidLoad does not guarantee the view has laid out its constraints. So when blueContainer's frame and size is zero, you will not see any effect on redContainer. You should use viewDidLayoutSubviews to get the correct frame and size from blueContainer.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
redContainer.frame.origin.x = view.frame.width/2
redContainer.frame.size.width = view.frame.width
//try to line up y with origin and size
redContainer.frame.origin.y = blueContainer.frame.origin.y
redContainer.frame.size.height = blueContainer.frame.size.height
}
I have a UIViewScroll(background color is blue) in view controller. I need a UIView(background color is white) that were from Xib. The Xib view has a UILabel(background color is green) with constraints. Now, the problem is UILabel constraints not applied after adding it to scrollView. How to add UIView without loss of constraints? Refer following screenshots and code.
Note:
I need to just update constraints of the sub views of the UIView without using IBOutlets of NSLayoutConstraints.
UIView on Xib:
Code:
class ViewController: UIViewController {
#IBOutlet var scrollView: UIScrollView!
var profileView:UIView!
override func viewDidLoad() {
super.viewDidLoad()
self.profileView = UINib.init(nibName: "ProfileView", bundle: nil).instantiate(withOwner: self)[0] as! UIView
self.scrollView.addSubview(self.profileView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.profileView.layer.frame.size = CGSize(width: self.scrollView.frame.width, height: self.profileView.frame.height)
self.profileView.layer.position = CGPoint(x: self.scrollView.frame.width/2, y: (self.profileView.frame.height/2)+10)
}
}
Output:
Update: More Information
I am aware to set contentSize of the scrollView. I used layer properties of UIView for manipulating height and width of the UIView. Instead of changing height and width also I need to update constraints of the sub views of UIView.
This is an example for understanding. But, In real I will be add more views like that.
Github Repository :
https://github.com/RAJAMOHAN-S/ScrollViewTest
Required output:
Your best bet is to set the constraint of self.profileView programmatically, I've added an example below to get you started.
class TestVC: UIViewController {
#IBOutlet var scrollView: UIScrollView!
private var profileView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
self.profileView = UINib.init(nibName: "ProfileView", bundle: nil).instantiate(withOwner: self)[0] as! UIView
self.configureProfileView()
}
private func configureProfileView() -> Void {
self.profileView.translatesAutoresizingMaskIntoConstraints = false
self.scrollView.addSubview(self.profileView)
self.profileView.widthAnchor.constraint(equalTo: self.scrollView.widthAnchor).isActive = true
self.profileView.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
// Pin the profile view to the top of the scrollView
self.profileView.topAnchor.constraint(equalTo: self.scrollView.topAnchor).isActive = true
}
}
More information can be found here too: https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/ProgrammaticallyCreatingConstraints.html
I have created a UIStackView in IB which has the distribution set to Fill Equally. I am looking to get the frame for each subView but the following code always returns (0, 0, 0, 0).
class ViewController: UIViewController {
#IBOutlet weak var stackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
let pView = UIView()
let sView = UIView()
pView.backgroundColor = UIColor.red
sView.backgroundColor = UIColor.orange
stackView.addArrangedSubview(pView)
stackView.addArrangedSubview(sView)
}
override func viewDidLayoutSubviews() {
print(stackView.arrangedSubviews[0].frame)
print(stackView.arrangedSubviews[1].frame)
}
}
I would think that a stack view set to fill equally would automatically set the calculate it.
Any help would be appreciated.
After reading over your code I think this is just a misunderstanding of viewDidLayoutSubviews(). Basically it is called when all the views that are descendants of the main view have been laid out but this does not include the subviews(descendants) of these views. See discussion notes from Apple.
"When the bounds change for a view controller's view, the view adjusts the positions of its subviews and then the system calls this method. However, this method being called does not indicate that the individual layouts of the view's subviews have been adjusted. Each subview is responsible for adjusting its own layout."
Now there are many ways to get the frame of the subviews with this being said.
First you could add one line of code in viewdidload and get it there.
override func viewDidLoad() {
super.viewDidLoad()
let pView = UIView()
let sView = UIView()
pView.backgroundColor = UIColor.red
sView.backgroundColor = UIColor.orange
stackView.addArrangedSubview(pView)
stackView.addArrangedSubview(sView)
stackView.layoutIfNeeded()
print(stackView.arrangedSubviews[0].frame)
print(stackView.arrangedSubviews[1].frame)
}
OR you can wait until viewDidAppear and check there.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(stackView.arrangedSubviews[0].frame)
print(stackView.arrangedSubviews[1].frame)
}
I have a UIViewController called LoginViewController. I want to build the view of that LoginViewController fully programmatically in a custom UIView class called LoginView instead of building all the elements within my LoginViewController. This way I'm preventing "View" code in a Controller class (MVC).
In the code below I'm setting the view of my LoginViewController to my LoginView which for simplicity only contains 2 UILabels
class LoginViewController: UIViewController {
override func loadView() {
super.loadView()
self.view = LoginView(frame: CGRect.zero)
}
The LoginView class initialises both labels and should set some constraints.
class LoginView: UIView {
var usernameLabel: UILabel!
var passwordLabel: UILabel!
override init (frame : CGRect) {
super.init(frame : frame)
setupLabels()
}
convenience init () {
self.init(frame:CGRect.zero)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func setupLabels(){
//Init labels and set a simple text
self.usernameLabel = UILabel()
self.usernameLabel.text = "Username"
self.passwordLabel = UILabel()
self.passwordLabel.text = "Password"
//Set constraints which aren't possible since there is no contentView, perhaps using the frame?
}
}
This doesn't work since the view's bounds are 0. However I couldn't find any resource that gives insight in whether this is possible, so I tried my approach which didn't work.
How you set the view of a UIViewController to a custom UIView which is made programmatically? Or is the above snippet recommended?
This is the working solution based on Jadar's answer:
class LoginViewController: UIViewController {
override func loadView() {
view = LoginView()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
class LoginView: UIView {
var usernameLabel: UILabel!
var passwordLabel: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
self.usernameLabel = UILabel()
self.usernameLabel.text = "Username"
self.passwordLabel = UILabel()
self.passwordLabel.text = "Password"
addSubview(usernameLabel)
addSubview(passwordLabel)
if let superview = usernameLabel.superview{
//Setting AutoLayout using SnapKit framework
usernameLabel.snp.makeConstraints { (make) in
make.center.equalTo(superview)
}
}
}
Result:
It looks there are really two questions here. One, what is the best way to programmatically set up a ViewController. The other, how to set up a View programmatically.
First, The best way to have a ViewController programmatically use a different UIView subclass is to initialize and assign it in the loadView method. Per Apple's docs:
You can override this method in order to create your views manually.
If you choose to do so, assign the root view of your view hierarchy to
the view property. The views you create should be unique instances and
should not be shared with any other view controller object. Your
custom implementation of this method should not call super.
This would look something like this:
class LoginViewController: UIViewController {
override func loadView() {
// Do not call super!
view = LoginView()
}
}
This way you shouldn't have to deal with sizing it, as the View Controller itself should take care of it (as it does with it's own UIView).
Remember, do not call super.loadView() or the controller will be confused. Also, the first time I tried this I got a black screen because I forgot to call window.makeKeyAndVisible() in my App Delegate. In this case the view was never even added to the window hierarchy. You can always use the view introspecter button in Xcode to see what's going on.
Second, you will need to call self.addSubview(_:) in your UIView subclass in order to have them appear. Once you add them as subviews, you can add constraints with NSLayoutConstraint.
private func setupLabels(){
// Initialize labels and set their text
usernameLabel = UILabel()
usernameLabel.text = "Username"
usernameLabel.translatesAutoresizingMaskIntoConstraints = false // Necessary because this view wasn't instantiated by IB
addSubview(usernameLabel)
passwordLabel = UILabel()
passwordLabel.text = "Password"
passwordLabel.translatesAutoresizingMaskIntoConstraints = false // Necessary because this view wasn't instantiated by IB
addSubview(passwordLabel)
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|-10-[view]", options: [], metrics: nil, views: ["view":usernameLabel]))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|-20-[view]", options: [], metrics: nil, views: ["view":passwordLabel]))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-10-[view]", options: [], metrics: nil, views: ["view":usernameLabel]))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-20-[view]", options: [], metrics: nil, views: ["view":passwordLabel]))
}
For more info on the visual format language used to create the constraints, see the VFL Guide
Override the layoutSubviews method to update the frames of the subviews inside your custom view.
And never call super.loadView(). This is documented for the loadView method.
You should load the custom view when LoginViewController's layout constraints are already loaded, try this:
override func viewDidLoad() {
super.viewDidLoad()
let newView = LoginView(frame: view.bounds)
view.addSubview(newView)
}
In your Viewcontroller's loadView method do this:
class LoginViewController: UIViewController {
override func loadView() {
super.view = LoginView()
}
}
In your UIView's custom class do this:
class LoginView: UIView {
convenience init() {
self.init(frame: UIScreen.main.bounds)
setupLabels()
}
}
Now your UIView has a frame , and you can setup all your views through code by providing them frames.