I am trying create a UITextField programmatically, however it does not appear in the view.
I believe I have set the constraints correctly and they should all be activated on viewDidLoad. I am sure I have missed something obvious however for the life of me cannot understand what.
class HomeController: UIViewController {
let textField: UITextField = {
let input = UITextField()
input.translatesAutoresizingMaskIntoConstraints = false
return input
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .purple
view.addSubview(textField)
NSLayoutConstraint.activate([
textField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
textField.centerYAnchor.constraint(equalTo: view.centerYAnchor),
textField.widthAnchor.constraint(equalToConstant: 300),
textField.heightAnchor.constraint(equalToConstant: 40)
])
}
}
Set a background property and it will show.
let textField: UITextField = {
let input = UITextField()
input.backgroundColor = UIColor.white
input.translatesAutoresizingMaskIntoConstraints = false
return input
}()
Related
I’m trying to get the tableView to move up when the search bar does. Take a look at the problem:
I think I see what the issue is here, but I can't think of a solution. In SearchResultsUpdating I have an animation block:
func updateSearchResults(for searchController: UISearchController) {
UIView.animateKeyframes(withDuration: 1, delay: 0, options: UIView.KeyframeAnimationOptions(rawValue: 7)) {
self.tableView.frame = CGRect(x: 20, y: self.view.safeAreaInsets.top, width:
self.view.frame.size.width-40, height: self.view.frame.size.height -
self.view.safeAreaInsets.top)
}
}
It seems to me that the animation block is only receiving the previous coordinates for the y origin, hence it is animating out of sync. I tried adding a target to the tableView, or navigationBar, or the searchBarTextField instead, but nothing worked.
Any help is appreciated, thanks!
EDIT: After implementing Shawn's second suggestion this was the result:
I can't imagine why it isn't animating smoothly now... very frustrating!
EDIT 2 - Requested Code:
class ViewController: UIViewController{
//City TableView
let cityTableView = UITableView()
let searchVC: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.obscuresBackgroundDuringPresentation = true
searchController.searchBar.placeholder = "Search"
return searchController
}()
//viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
//Do any setup for the view controller here
setupViews()
//CityViewController
setupCityViewTableView()
}
//setupViews
func setupViews(){
//NAVIGATIONBAR:
//title
title = "Weather"
//set to hidden because on initial load there is a scroll view layered over top of the CityViewTableView (code not shown here). This gets set to false when the scrollView alpha is set to 0 and the CityViewTableView is revealed
navigationController?.navigationBar.isHidden = true
navigationController?.navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
//NAVIGATION ITEM:
navigationItem.searchController = searchVC
//UISEARCHBARCONTROLLER:
searchVC.searchResultsUpdater = self
}
}
//MARK: -CityViewController Functions
extension ViewController{
//setUp TableView
func setupCityViewTableView(){
cityTableView.translatesAutoresizingMaskIntoConstraints = false
//set tableView delegate and dataSource
cityTableView.delegate = self
cityTableView.dataSource = self
//background color
cityTableView.backgroundColor = .black
//separator color
cityTableView.separatorColor = .clear
//is transparent on initial load
cityTableView.alpha = 0
//set tag
cityTableView.tag = 1000
//hide scroll indicator
cityTableView.showsVerticalScrollIndicator = false
//register generic cell
cityTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cityCell")
//add subview
view.addSubview(cityTableView)
//Auto Layout
cityTableView.leadingAnchor
.constraint(equalTo: view.leadingAnchor,
constant: 20).isActive = true
cityTableView.topAnchor
.constraint(equalTo: view.topAnchor,
constant: 0).isActive = true
cityTableView.trailingAnchor
.constraint(equalTo: view.trailingAnchor,
constant: -20).isActive = true
cityTableView.bottomAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: 0).isActive = true
}
}
//MARK: -TableView Controller
extension ViewController: UITableViewDelegate,
UITableViewDataSource{
//number of rows
func tableView(_ tableView: UITableView, numberOfRowsInSection
section: Int) -> Int {
if tableView.tag == 1000{
return 5
}
return self.models[tableView.tag].count
}
//cell for row
func tableView(_ tableView: UITableView, cellForRowAt indexPath:
IndexPath) -> UITableViewCell {
//CityViewController
if tableView.tag == 1000{
let cell = tableView.dequeueReusableCell(withIdentifier:
"cityCell", for: indexPath)
cell.textLabel?.text = "Test"
cell.textLabel?.textAlignment = .center
cell.backgroundColor = .systemGray
cell.selectionStyle = .none
cell.layer.cornerRadius = 30
cell.layer.borderColor = UIColor.black.cgColor
cell.layer.borderWidth = 5
cell.layer.cornerCurve = .continuous
return cell
}
//WeatherViewController
//code here for scrollView tableViews
}
//Height for row
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if tableView.tag == 1000{
return view.frame.size.height/7
}
return view.frame.size.height/10
}
//Should Highlight Row
func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
if tableView.tag == 1000{
return true
}
return false
}
//Did select row
func tableView(_ tableView: UITableView, didSelectRowAt
indexPath: IndexPath) {
//calls function for segue to Weather Scroll View (not shown)
if tableView.tag == 1000{
segueToWeatherView(indexPath: indexPath)
}
}
}
EDIT 3: When I comment out another function it finally works, but I'm not sure exactly why, or how to fix it. This is the function in question, addSubViews()
//setup viewController
func addSubViews(){
//add weatherView as subView of ViewController
view.addSubview(weatherView)
//add subviews to weatherView
weatherView.addSubview(scrollView)
weatherView.addSubview(pageControl)
weatherView.addSubview(segueToCityViewButton)
weatherView.addSubview(segueToMapViewButton)
}
Specifically, it works when I comment out this line:
view.addSubview(weatherView)
Here is all the code concerning the setting up of the weatherView and all of its subViews:
//Any additional setup goes here
private func setupViews(){
//VIEWCONTROLLER:
//title
title = "Weather"
//Background color of view Controller
view.backgroundColor = .darkGray
//WEATHERVIEW:
//Background color of weather view Controller
weatherView.backgroundColor = .clear
//weatherView frame
weatherView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height)
//SCROLLVIEW:
//background color of scroll view
scrollView.backgroundColor = .clear
//scrollView frame
scrollView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height)
//changed
//PAGECONTROL:
//page control frame
pageControl.frame = CGRect(x: 0, y: view.frame.height-view.frame.size.height/14, width: view.frame.width, height: view.frame.size.height/14)
//TRANSITIONVIEW:
//TransitionView frame
transitionView.frame = CGRect(x: 20, y: 0, width: view.frame.size.width-40, height: view.frame.size.height)
//BUTTONS:
//segue to CityView
segueToCityViewButton.frame = CGRect(x: (weatherView.frame.width/5*4)-20, y: weatherView.frame.height-weatherView.frame.size.height/14, width: weatherView.frame.width/5, height: pageControl.frame.height)
//segue to MapView:
segueToMapViewButton.frame = CGRect(x: 20, y: weatherView.frame.height-weatherView.frame.size.height/14, width: weatherView.frame.width/5, height: pageControl.frame.height)
//LABELS:
transitionViewLabel.frame = transitionView.bounds
//NAVIGATIONBAR:
//set to hidden on initial load
navigationController?.navigationBar.isHidden = true
navigationController?.navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
//NAVIGATION ITEM:
navigationItem.searchController = searchVC
//UISEARCHBARCONTROLLER:
searchVC.searchResultsUpdater = self
}
For the sake of being thorough, here is the full viewDidLoad() Function:
override func viewDidLoad() {
super.viewDidLoad()
//MARK: View Controller
//These two will eventually be moved to the DispatchQueue in APICalls.swift
configureScrollView()
pageControl.numberOfPages = models.count
//Do any setup for the view controller here
setupViews()
//setup ViewController
addSubViews()
//Add Target for the pageControl
addTargetForPageControl()
//MARK: CityViewController
setupCityViewTableViews()
}
EDIT 4: With the following changes in viewDidLoad(), I finally got it to work!
override func viewDidLoad() {
super.viewDidLoad()
//MARK: CityViewController
//Moved to a position before setting up the other views
setupCityViewTableViews()
//MARK: View Controller
//These two will eventually be moved to the DispatchQueue in APICalls.swift
configureScrollView()
pageControl.numberOfPages = models.count
//Do any setup for the view controller here
setupViews()
//setup ViewController
addSubViews()
//Add Target for the pageControl
addTargetForPageControl()
}
Doing it the way you are doing it right now is a way to do it but I think it is the most challenging way to do it for several reasons:
You don't have much control and access to the implementation of the search controller animation within the navigation bar so getting the right coordinates might be a task
Even if you did manage to get the right coordinates, trying to synchronize your animation frames and timing to look in sync and seamless with the search animation on the nav bar will be tricky
I suggest the 2 following alternatives to what you are currently doing where you will get the news experience pretty much for free out of the box.
Option 1: Use a UITableViewController instead of a UIViewController
This is all the code using a UITableViewController and adding a UISearchController to the navigation bar.
class NewsTableViewVC: UITableViewController
{
private let searchController: UISearchController = {
let sc = UISearchController(searchResultsController: nil)
sc.obscuresBackgroundDuringPresentation = false
sc.searchBar.placeholder = "Search"
sc.searchBar.autocapitalizationType = .allCharacters
return sc
}()
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = .black
title = "Weather"
// Ignore this as you have you own custom cell class
tableView.register(CustomCell.self,
forCellReuseIdentifier: CustomCell.identifier)
setUpNavigationBar()
}
private func setUpNavigationBar()
{
navigationItem.searchController = searchController
}
}
This is the experience you can expect
Option 2: Use auto layouts rather than frames to configure your UITableView
If you don't want to use a UITableViewController, configure your UITableView using auto layout rather than frames which has a little more work but not too much:
class NewsTableViewVC: UIViewController, UITableViewDataSource, UITableViewDelegate
{
private let searchController: UISearchController = {
let sc = UISearchController(searchResultsController: nil)
sc.obscuresBackgroundDuringPresentation = false
sc.searchBar.placeholder = "Search"
sc.searchBar.autocapitalizationType = .allCharacters
return sc
}()
private let tableView = UITableView()
override func viewDidLoad()
{
super.viewDidLoad()
// Just to show it's different from the first
view.backgroundColor = .purple
title = "Weather"
setUpNavigationBar()
setUpTableView()
}
private func setUpNavigationBar()
{
navigationItem.searchController = searchController
}
private func setUpTableView()
{
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(CustomCell.self,
forCellReuseIdentifier: CustomCell.identifier)
tableView.dataSource = self
tableView.delegate = self
tableView.backgroundColor = .clear
view.addSubview(tableView)
// Auto Layout
tableView.leadingAnchor
.constraint(equalTo: view.leadingAnchor,
constant: 0).isActive = true
// This important, configure it to the top of the view
// NOT the safe area margins to get the desired result
tableView.topAnchor
.constraint(equalTo: view.topAnchor,
constant: 0).isActive = true
tableView.trailingAnchor
.constraint(equalTo: view.trailingAnchor,
constant: 0).isActive = true
tableView.bottomAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: 0).isActive = true
}
}
You can expect the following experience:
Update
This is based on your updated code, you missed one small detail which might be impacting the results you see and this is the top constraint of the UITableView.
You added the constraint to the safeAreaLayoutGuide top anchor:
cityTableView.topAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
constant: 0).isActive = true
My recommendation from the code above if you notice is to add it to the view top constraint
// This important, configure it to the top of the view
// NOT the safe area margins to get the desired result
cityTableView.topAnchor
.constraint(equalTo: view.topAnchor,
constant: 0).isActive = true
Give this a go and see if you come close to getting what you expect ?
Here is a link to the complete code of my implementation if it helps:
I'm having trouble setting constraints to a label programmatically in Swift. I want the label to fill the entire screen. But I dont know how to do.
Thank you for your help.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
label.text = "Hello"
label.backgroundColor = UIColor.yellow
self.view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
let width: NSLayoutConstraint
width = label.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 1)
let top: NSLayoutConstraint
top = label.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor)
let bottom: NSLayoutConstraint
bottom = label.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor)
width.isActive = true
bottom.isActive = true
top.isActive = true
}
}
You can use this extension for all of your views
extension UIView {
public func alignAllEdgesWithSuperview() {
guard let superview = self.superview else {
fatalError("add \(self) to a superview first.")
}
self.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
self.leadingAnchor.constraint(equalTo: superview.leadingAnchor),
self.trailingAnchor.constraint(equalTo: superview.trailingAnchor),
self.topAnchor.constraint(equalTo: superview.topAnchor),
self.bottomAnchor.constraint(equalTo: superview.bottomAnchor)
]
NSLayoutConstraint.activate(constraints)
}
}
and the usage
view.addSubview(someView)
someView.alignAllEdgesWithSuperview()
I have a simple log in view implemented as follows :
import UIKit
class LoginViewController: UIViewController {
private var safeArea : UILayoutGuide!
private let scrollView : UIScrollView = {
let view = UIScrollView()
view.translatesAutoresizingMaskIntoConstraints = false
view.keyboardDismissMode = .onDrag
return view
}()
private let containerView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let logoView : UIImageView = {
let view = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleAspectFill
view.layer.cornerRadius = 8
view.image = UIImage(named: "logo")!
return view
}()
private let emailOrPhoneTextFieldView : UITextField = {
let view = UITextField()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.borderColor = UIColor.lightGray.cgColor
view.layer.borderWidth = 0.5
view.layer.cornerRadius = 10
view.placeholder = "Email or phone"
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.textColor = .black
view.autocapitalizationType = .none
view.tintColor = UIColor(named: "myColor")
view.backgroundColor = .systemGray
return view
}()
private let passwordTextFieldView : UITextField = {
let view = UITextField()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.borderColor = UIColor.lightGray.cgColor
view.layer.borderWidth = 0.5
view.layer.cornerRadius = 10
view.placeholder = "Password"
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.textColor = .black
view.autocapitalizationType = .none
view.tintColor = UIColor(named: "myColor")
view.isSecureTextEntry = true
view.backgroundColor = .systemGray
return view
}()
private let logInButtonView : UIButton = {
let view = UIButton()
view.setTitle("Log in", for: .normal)
view.setTitleColor(.white, for : .normal)
view.setBackgroundImage( UIImage(named: "blue_pixel")!, for: .normal)
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
view.translatesAutoresizingMaskIntoConstraints = false
view.addTarget(self, action: #selector(logInButtonClickedHandler), for: .touchUpInside)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
safeArea = view.layoutMarginsGuide
setupViews()
}
private func setupViews()
{
view.addSubview(scrollView)
containerView.addSubview(logoView)
containerView.addSubview(emailOrPhoneTextFieldView)
containerView.addSubview(passwordTextFieldView)
containerView.addSubview(logInButtonView)
scrollView.addSubview(containerView)
let constraints = [
scrollView.topAnchor.constraint(equalTo: safeArea.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
containerView.topAnchor.constraint(equalTo: scrollView.topAnchor),
containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
logoView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 120),
logoView.widthAnchor.constraint(equalToConstant: 100),
logoView.heightAnchor.constraint(equalToConstant: 100),
logoView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
emailOrPhoneTextFieldView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
emailOrPhoneTextFieldView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
emailOrPhoneTextFieldView.topAnchor.constraint(equalTo: logoView.bottomAnchor, constant: 120),
emailOrPhoneTextFieldView.heightAnchor.constraint(equalToConstant: 50),
passwordTextFieldView.topAnchor.constraint(equalTo: emailOrPhoneTextFieldView.bottomAnchor),
passwordTextFieldView.leadingAnchor.constraint(equalTo: emailOrPhoneTextFieldView.leadingAnchor),
passwordTextFieldView.heightAnchor.constraint(equalToConstant: 50),
passwordTextFieldView.trailingAnchor.constraint(equalTo: emailOrPhoneTextFieldView.trailingAnchor),
logInButtonView.topAnchor.constraint(equalTo: passwordTextFieldView.bottomAnchor, constant: 16),
logInButtonView.leadingAnchor.constraint(equalTo: passwordTextFieldView.leadingAnchor),
logInButtonView.trailingAnchor.constraint(equalTo: passwordTextFieldView.trailingAnchor),
logInButtonView.heightAnchor.constraint(equalToConstant: 50),
logInButtonView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
]
NSLayoutConstraint.activate(constraints)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow(notification:)),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow(notification:)),
name: UIResponder.keyboardWillHideNotification,
object: nil)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
#objc private func logInButtonClickedHandler() {
print("button pressed")
}
}
//MARK: Keyboard Notifications
private extension LoginViewController {
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
scrollView.contentInset.bottom = keyboardSize.height
scrollView.verticalScrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0)
}
}
#objc func keyboardWillHide(notification: NSNotification) {
scrollView.contentInset.bottom = .zero
scrollView.verticalScrollIndicatorInsets = .zero
}
}
Everything is fine with the implementation but 2 things looks very strange for me and I guess I misunderstood smth
If I comment out
containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
I see that my container view does not fit the whole screen width (actually it's about 50% of it)
Why? I set trailing and leading constraints to scrollview, which is 100% of view width.
If I comment out
logInButtonView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
I don't get button click events and I'm not able to input anything inside textfields. What is the issue here?
From the Apple Docs:
Constraints between the edges or margins of the scroll view and its
content attach to the scroll view’s content area.
Constraints between the height, width, or centers attach to the scroll
view’s frame.
Hence you need the width constraint in order to make the contentView the full width of the ScrollView's frame.
As above, without that constraint the contentView only has constraints to the top/bottom edge of the scrollView this doesn't define its height and so you need to add full top-to-bottom constraints on the subviews of the contentView in order to define its height.
If you use the View Hierarchy Debugger you'll see the contentView has 0 height without that constraint (it just isn't clipping the content), hence why you can't tap on any controls.
It's worth giving the 'Working with Scroll Views' section of Apple Auto-Layout docs a read.
After struggling with programmatic dynamic UI elements for the last few weeks I decided I would give UIstackView a try.
I want custom UIView classes to occupy the stackview with different heights based upon user Input I would remove, add views on the fly.
I found out that stackViews base their 'cell' height upon the UI element's intrinsic content size. However, UIViews do not have one. I searched far and wide and found out that I need to override the View's intrisicContentsize function with one where I can explicitly set the width and height.
However results are very unpredictable and I'm sure there is some little thing that I just do not know dat causes this weird behaviour. Since I'm new to the language and there are a LOT of gotcha's I'm just gonna paste the code here and hope you'll be able to spot what I'm doing wrong.
I've read the docs ofc, a lot of articles, they all point to that override funcion that does not seem to work form me.
This is my mainViewController class;
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
var bv = BackButtonView(frame: CGRect.zero, image: UIImage(named: "backArrow.png")!)
var redView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}();
var blueView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .blue
return view
}();
let stack : UIStackView = {
let stack = UIStackView();
stack.translatesAutoresizingMaskIntoConstraints = false;
stack.axis = .vertical
stack.distribution = .fillProportionally;
stack.spacing = 8
return stack;
}();
override func viewDidLoad() {
let view = UIView(frame: UIScreen.main.bounds)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
self.view = view
self.view.addSubview(stack)
createLayout()
bv.intrinsicContentSize
stack.addArrangedSubview(bv)
print(bv.frame)
print(bv.intrinsicContentSize)
stack.layoutIfNeeded()
stack.addArrangedSubview(redView)
stack.addArrangedSubview(blueView)
}
private func setConstraints(view: UIView) -> [NSLayoutConstraint] {
return [
view.heightAnchor.constraint(equalToConstant: 50),
]
}
private func createLayout() {
stack.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
stack.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
stack.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
stack.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
}
}
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = MyViewController()
Now here's my custom UIClass that is giving me all this trouble:
import UIKit
public class BackButtonView : UIView {
public var size = CGSize(width: 10, height: UIView.noIntrinsicMetric)
override open var intrinsicContentSize: CGSize {
return size
}
var button : UIButton;
public init(frame: CGRect, image: UIImage) {
button = UIButton()
super.init(frame : frame);
self.translatesAutoresizingMaskIntoConstraints = false;
self.backgroundColor = .black
self.addSubview(button)
setupButton(image: image);
print(button.intrinsicContentSize)
}
let backButtonTrailingPadding : CGFloat = -18
lazy var buttonConstraints = [
button.heightAnchor.constraint(equalToConstant: 50),
button.widthAnchor.constraint(equalToConstant: 50),
button.centerYAnchor.constraint(equalTo: self.centerYAnchor),
button.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: backButtonTrailingPadding),
]
private func setupButton(image: UIImage) {
self.button.translatesAutoresizingMaskIntoConstraints = false;
self.button.setImage(image, for: .normal);
self.button.contentMode = .scaleToFill
NSLayoutConstraint.activate(buttonConstraints)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
struct anchors {
var topAnchor = NSLayoutConstraint()
var bottomAnchor = NSLayoutConstraint()
var leadingAnchor = NSLayoutConstraint()
var trailingAnchor = NSLayoutConstraint()
}
}
You can see that the specified width in the size proprrty gets ignored.
Under the above conditions., this is the output
If I now change the my custom UIClass's intrisiContentSize height to anyting else, this happens:
public var size = CGSize(width: UIView.noIntrinsicMetric, height: 10)
override open var intrinsicContentSize: CGSize {
return size
}
result:
Please help me figure out what is and isn't going on
The final screen should look something like this:
I have a horizontal UIStackView that, by default, looks as follows:
The view with the heart is initially hidden and then shown at runtime. I would like to reduce the spacing between the heart view and the account name view.
The following code does the job, but only, when executed in viewDidLoad:
stackView.setCustomSpacing(8, after: heartView)
When changing the custom spacing later on, say on a button press, it doesn't have any effect. Now, the issue here is, that the custom spacing is lost, once the subviews inside the stack view change: when un-/hiding views from the stack view, the custom spacing is reset and cannot be modified.
Things, I've tried:
verified the spacing is set by printing stackView.customSpacing(after: heartView) (which properly returns 8)
unsuccessfully ran several reload functions:
stackView.layoutIfNeeded()
stackView.layoutSubviews()
view.layoutIfNeeded()
view.layoutSubviews()
viewDidLayoutSubviews()
How can I update the custom spacing of my stack view at runtime?
You need to make sure the UIStackView's distribution property is set to .fill or .fillProportionally.
I created the following swift playground and it looks like I am able to use setCustomSpacing at runtime with random values and see the effect of that.
import UIKit
import PlaygroundSupport
public class VC: UIViewController {
let view1 = UIView()
let view2 = UIView()
let view3 = UIView()
var stackView: UIStackView!
public init() {
super.init(nibName: nil, bundle: nil)
}
public required init?(coder aDecoder: NSCoder) {
fatalError()
}
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view1.backgroundColor = .red
view2.backgroundColor = .green
view3.backgroundColor = .blue
view2.isHidden = true
stackView = UIStackView(arrangedSubviews: [view1, view2, view3])
stackView.spacing = 10
stackView.axis = .horizontal
stackView.distribution = .fillProportionally
let uiSwitch = UISwitch()
uiSwitch.addTarget(self, action: #selector(onSwitch), for: .valueChanged)
view1.addSubview(uiSwitch)
uiSwitch.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
uiSwitch.centerXAnchor.constraint(equalTo: view1.centerXAnchor),
uiSwitch.centerYAnchor.constraint(equalTo: view1.centerYAnchor)
])
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.heightAnchor.constraint(equalToConstant: 50),
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50)
])
}
#objc public func onSwitch(sender: Any) {
view2.isHidden = !view2.isHidden
if !view2.isHidden {
stackView.setCustomSpacing(CGFloat(arc4random_uniform(40)), after: view2)
}
}
}
PlaygroundPage.current.liveView = VC()
PlaygroundPage.current.needsIndefiniteExecution = true
Another reason setCustomSpacing can fail is if you call it before adding the arranged subview after which you want to apply the spacing.
Won't work:
headerStackView.setCustomSpacing(50, after: myLabel)
headerStackView.addArrangedSubview(myLabel)
Will work:
headerStackView.addArrangedSubview(myLabel)
headerStackView.setCustomSpacing(50, after: myLabel)
I also noticed that custom spacing values get reset after hiding/unhiding children. I was able to override updateConstraints() for my parent view and set the custom spacing as needed. The views then kept their intended spacing.
override func updateConstraints() {
super.updateConstraints()
stackView.setCustomSpacing(10, after: childView)
}