As a simple starting point. I'm trying to create a custom button that has an activity indicator in the middle.
After a tap - it will indicate that it's thinking
I'm pretty much stuck at step 1 - adding the indicator to be centred in the surrounding view.
No matter want constraints I try it always appears in the top left corner.
What am I missing?
Here's my playground code.
//: Playground - noun: a place where people can play
import PlaygroundSupport
import UIKit
class ConnectButton : UIView {
fileprivate let activityIndicator: UIActivityIndicatorView = {
let activityIndicator = UIActivityIndicatorView()
activityIndicator.color = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
activityIndicator.hidesWhenStopped = true;
activityIndicator.stopAnimating();
activityIndicator.activityIndicatorViewStyle = .white;
return activityIndicator;
}()
private func initView() {
translatesAutoresizingMaskIntoConstraints = true;
addSubview(activityIndicator);
NSLayoutConstraint.activate([
activityIndicator.centerYAnchor.constraint(equalTo: self.centerYAnchor),
activityIndicator.leftAnchor.constraint(equalTo: self.centerXAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder);
self.initView();
}
override init(frame: CGRect) {
super.init(frame: frame);
self.initView();
}
public func startAnimating() {
activityIndicator.startAnimating();
}
public func stopAnimating() {
activityIndicator.stopAnimating();
}
}
let dimensions = (width:200, height: 50);
let connectButton = ConnectButton(
frame: CGRect(
x: dimensions.width / 2,
y: dimensions.height / 2,
width: dimensions.width,
height: dimensions.height
)
)
connectButton.startAnimating();
connectButton.backgroundColor = #colorLiteral(red: 0.1764705926, green: 0.4980392158, blue: 0.7568627596, alpha: 1);
PlaygroundPage.current.liveView = connectButton
You want to set translatesAutoresizingMaskIntoConstraints to false onto UIActivityIndicatorView. Otherwise, UIKit will create constraints based on the resizing masks set on the, which will conflict with the ones you already added.
fileprivate let activityIndicator: UIActivityIndicatorView = {
let activityIndicator = UIActivityIndicatorView()
activityIndicator.color = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
activityIndicator.hidesWhenStopped = true
activityIndicator.stopAnimating()
activityIndicator.activityIndicatorViewStyle = .white
activityIndicator. translatesAutoresizingMaskIntoConstraints = false
return activityIndicator
}()
Another solution: Create your UIActivityIndicator with a given frame from your superView as you already do, and simply call:
activityIndicator.center = self.center
And avoid the NSLayoutConstraints alltogether, unless you really need them for something.
However, as I mentioned in the commentsection, make sure you have your UIView laid out to the correct size before calling above line. Otherwise your result will be exactly the same.
Related
Slider with two different colors
How can we make a slider with two fixed colors? The colors won't change even if the slider is moving. Also, the slider thumb should be able to side over any of those two colors. I should be able to define the length of the first section.
func createSlider(slider:UISlider) {
let frame = CGRect(x: 0.0, y: 0.0, width: slider.bounds.width, height: 4)
let tgl = CAGradientLayer()
tgl.frame = frame
tgl.colors = [UIColor.gray.cgColor,UIColor.red.cgColor]
tgl.endPoint = CGPoint(x: 0.4, y: 1.0)
tgl.startPoint = CGPoint(x: 0.0, y: 1.0)
UIGraphicsBeginImageContextWithOptions(tgl.frame.size, false, 0.0)
tgl.render(in: UIGraphicsGetCurrentContext()!)
let backgroundImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
slider.setMaximumTrackImage(backgroundImage?.resizableImage(withCapInsets:.zero), for: .normal)
slider.setMinimumTrackImage(backgroundImage?.resizableImage(withCapInsets:.zero), for: .normal)
}
I have already tried this. But this dosent give me exactly what I wanted to achieve.
Here's one approach...
set both min and max track images to "empty" images
add left and right subviews to act as "fake" track images
add a different color sublayer on each side subview
when you slide the thumb, update the frames of the layers
Here's some example code... you may want to do some tweaks:
class XebSlider: UISlider {
private var colors: [[UIColor]] = [
[.black, .black],
[.black, .black],
]
// left and right views will become the "fake" track images
private let leftView = UIView()
private let rightView = UIView()
// each view needs a layer to "change color"
private let leftShape = CALayer()
private let rightShape = CALayer()
// constraints that will be updated
private var lvWidth: NSLayoutConstraint!
private var lvLeading: NSLayoutConstraint!
private var rvTrailing: NSLayoutConstraint!
// how wide the two "sides" should be
private var leftPercent: CGFloat = 0
private var rightPercent: CGFloat = 0
// track our current width, so we don't repeat constraint updates
// unless our width has changed
private var currentWidth: CGFloat = 0
init(withLeftSidePercent leftWidth: CGFloat, leftColors: [UIColor], rightColors: [UIColor]) {
super.init(frame: .zero)
commonInit()
leftView.backgroundColor = leftColors[1]
rightView.backgroundColor = rightColors[1]
leftShape.backgroundColor = leftColors[0].cgColor
rightShape.backgroundColor = rightColors[0].cgColor
leftPercent = leftWidth
rightPercent = 1.0 - leftPercent
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// set both track images to "empty" images
setMinimumTrackImage(UIImage(), for: [])
setMaximumTrackImage(UIImage(), for: [])
// add left and right subviews
[leftView, rightView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
v.layer.cornerRadius = 4.0
v.layer.masksToBounds = true
v.isUserInteractionEnabled = false
insertSubview(v, at: 0)
}
// add sublayers
leftView.layer.addSublayer(leftShape)
rightView.layer.addSublayer(rightShape)
// create constraints whose .constant values will be modified in layoutSubviews()
lvLeading = leftView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0)
rvTrailing = rightView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0)
lvWidth = leftView.widthAnchor.constraint(equalToConstant: 0.0)
// avoids auto-layout complaints when the frame changes (such as on device rotation)
lvWidth.priority = UILayoutPriority(rawValue: 999)
// set constraints for subviews
NSLayoutConstraint.activate([
lvLeading,
rvTrailing,
lvWidth,
leftView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 0.0),
leftView.heightAnchor.constraint(equalToConstant: 8.0),
rightView.centerYAnchor.constraint(equalTo: leftView.centerYAnchor),
rightView.heightAnchor.constraint(equalTo: leftView.heightAnchor),
rightView.leadingAnchor.constraint(equalTo: leftView.trailingAnchor, constant: 1.0),
])
}
override func layoutSubviews() {
super.layoutSubviews()
// we only want to do this if the bounds width has changed
if bounds.width != currentWidth {
let trackRect = self.trackRect(forBounds: bounds)
lvLeading.constant = trackRect.origin.x
rvTrailing.constant = -(bounds.width - (trackRect.origin.x + trackRect.width))
lvWidth.constant = trackRect.width * leftPercent
}
// get percentage of thumb position
// based on min and max values
let pctValue = (self.value - self.minimumValue) / (self.maximumValue - self.minimumValue)
// calculate percentage of each side that needs to be "covered"
// by the different color layer
let leftVal = max(0.0, min(CGFloat(pctValue) / leftPercent, 1.0))
let rightVal = max(0.0, min((CGFloat(pctValue) - leftPercent) / rightPercent, 1.0))
var rLeft = leftView.bounds
var rRight = rightView.bounds
rLeft.size.width = leftView.bounds.width * leftVal
rRight.size.width = rightView.bounds.width * rightVal
// disable default layer animations
CATransaction.begin()
CATransaction.setDisableActions(true)
leftShape.frame = rLeft
rightShape.frame = rRight
CATransaction.commit()
}
}
and a controller example showing its usage:
class ViewController: UIViewController {
var slider: XebSlider!
override func viewDidLoad() {
super.viewDidLoad()
let leftSideColors: [UIColor] = [
#colorLiteral(red: 0.4796532989, green: 0.4797258377, blue: 0.4796373844, alpha: 1),
#colorLiteral(red: 0.8382737041, green: 0.8332912326, blue: 0.8421040773, alpha: 1),
]
let rightSideColors: [UIColor] = [
#colorLiteral(red: 0.9009097219, green: 0.3499996662, blue: 0.4638580084, alpha: 1),
#colorLiteral(red: 0.9591985345, green: 0.8522816896, blue: 0.8730568886, alpha: 1),
]
let leftSideWidthPercent: CGFloat = 0.5
slider = XebSlider(withLeftSidePercent: leftSideWidthPercent, leftColors: leftSideColors, rightColors: rightSideColors)
view.addSubview(slider)
slider.translatesAutoresizingMaskIntoConstraints = false
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain slider 40-pts from Top / Leading / Trailing
slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
slider.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
])
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
// to get teh custom slider to update properly
self.slider.setNeedsLayout()
}, completion: {
_ in
})
}
}
Result:
I've been facing some difficulties trying to apply CAGradientLayers on two items in a view controller: a UIView and a UIButton. On investigation, when the gradient is applied to both items, only the UIButton has the gradient on it whereas the UIView appears transparent.
My gradient is defined as such:
import UIKit
struct K {
struct Design {
static let upGradientLayer: CAGradientLayer = {
let layer = CAGradientLayer()
layer.colors = [upBlue.cgColor, upPurple.cgColor]
layer.startPoint = CGPoint(x: 0.0, y: 0.0)
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
return layer
}()
static let upBlue = UIColor(named: "UP Blue") ?? .systemBlue
static let upPurple = UIColor(named: "UP Purple") ?? .systemPurple
}
}
The function that applies the gradients (I used insertSublayer) is separated from the main View Controller file. Here's the code for that function:
import UIKit
extension UIButton {
func applyButtonDesign(_ gradientLayer: CAGradientLayer) {
self.layer.insertSublayer(gradientLayer, at: 0)
self.layer.cornerRadius = 10.0
self.layer.masksToBounds = true
}
}
extension UIView {
func applyHeaderDesign(_ gradientLayer: CAGradientLayer) {
self.layer.insertSublayer(gradientLayer, at: 0)
self.layer.cornerRadius = 10.0
self.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
self.clipsToBounds = true
}
}
(Note: It appears that addSublayer doesn't work either.)
My UI is being created programmatically (without a Storyboard), and I'm fairly new to it. Here's the code for the view controller where the issue is happening:
import UIKit
class WelcomeViewController: UIViewController {
var headerView: UIView!
var descriptionLabel: UILabel!
var focusImageView: UIImageView!
var promptLabel: UILabel!
var continueButton: UIButton!
let headerGradientLayer = K.Design.upGradientLayer
let buttonGradientLayer = K.Design.upGradientLayer
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let guide = view.safeAreaLayoutGuide
// MARK: Header View
headerView = UIView()
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.applyHeaderDesign(headerGradientLayer)
view.addSubview(headerView)
NSLayoutConstraint.activate([
headerView.topAnchor.constraint(equalTo: view.topAnchor),
headerView.rightAnchor.constraint(equalTo: view.rightAnchor),
headerView.leftAnchor.constraint(equalTo: view.leftAnchor),
headerView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.25)
])
// MARK: Other items in ViewController
// MARK: Continue Button
continueButton = UIButton()
continueButton.translatesAutoresizingMaskIntoConstraints = false
continueButton.applyButtonDesign(buttonGradientLayer)
continueButton.setTitle("Let's go!", for: .normal)
continueButton.titleLabel?.font = UIFont(name: "Metropolis Bold", size: 22.0)
continueButton.titleLabel?.textColor = .white
continueButton.addTarget(nil, action: #selector(continueButtonPressed), for: .touchUpInside)
view.addSubview(continueButton)
NSLayoutConstraint.activate([
continueButton.topAnchor.constraint(equalTo: promptLabel.bottomAnchor, constant: K.Layout.someSpaceBetween),
continueButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -K.Layout.someSpaceBetween),
continueButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: K.Layout.someSpaceBetween),
continueButton.bottomAnchor.constraint(equalTo: guide.bottomAnchor),
continueButton.heightAnchor.constraint(equalToConstant: 50.0)
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
headerGradientLayer.frame = headerView.bounds
buttonGradientLayer.frame = continueButton.bounds
}
// MARK: - Functions
#objc func continueButtonPressed() {
let newViewController = NameViewController()
newViewController.modalPresentationStyle = .fullScreen
newViewController.hero.modalAnimationType = .slide(direction: .left)
self.present(newViewController, animated: true, completion: nil)
}
}
When running on the Simulator, I get the below image that shows the issue. Notice that the gradient is not applied on the UIView but is on the UIButton.
What's causing this issue to happen, and how can I resolve this? If there's a fundamental concept that needs to be learned to tackle this issue, do share as well.
Both headerGradientLayer and buttonGradientLayer refer to the same layer instance because upGradientLayer is a stored property, not a computed property (as I'm assuming you meant for it to be). Since they both refer to the same instance, when you call this:
continueButton.applyButtonDesign(buttonGradientLayer)
...which internally calls this:
self.layer.insertSublayer(gradientLayer, at: 0)
...it removes the gradient layer instance from its current view (your header view) and adds it to your button (because layers cannot have more than one superlayer). The result is that you only see the gradient on your button and not your header view.
I imagine you meant for upGradientLayer to be a computed property so that it returns a new layer instance every time it's called, like this:
static var upGradientLayer: CAGradientLayer {
let layer = CAGradientLayer()
layer.colors = [upBlue.cgColor, upPurple.cgColor]
layer.startPoint = CGPoint(x: 0.0, y: 0.0)
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
return layer
}
This is a custom view, this view creates a square with a given frame with the background color. I am adding a custom view to a subview, the view appears properly. But I am not able to cover the bottom safe area, anyone can help me to remove the safe area from bottom Programmatically.
class CustomView: UIView {
override var frame: CGRect {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.isOpaque = false
}
override func draw(_ rect: CGRect) {
UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.7).setFill()
UIRectFill(rect)
let square = UIBezierPath(rect: CGRect(x: 200, y: rect.size.height/2 - 150/2, width: UIScreen.main.bounds.size.width - 8, height: 150))
let dashPattern : [CGFloat] = [10, 4]
square.setLineDash(dashPattern, count: 2, phase: 0)
UIColor.white.setStroke()
square.lineWidth = 5
square.stroke()
}
}
Consider the following example:
class ViewController: UIViewController {
private let myView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
configureCustomView()
}
private func configureCustomView() {
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
myView.backgroundColor = .systemPurple
NSLayoutConstraint.activate([
myView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
myView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
myView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
myView.heightAnchor.constraint(equalToConstant: 200)
])
}
}
Result:
If you don't want to go over the safe area, then you could use myView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) inside NSLayoutConstraint.activate([...]).
So you actually don't have to remove the SafeArea, because you can simply ignore them if you want so...
In case you want to do this fast. (Not programatically)
Open storyboard.
Select your UIView.
Safe Area should be unselected.
So I am trying to constraint a view inside a UICollectionViewCell but it seems like the constraints are not applied at all
This is my custom cell class:
class MyCustomCell: UICollectionViewCell {
var message: String?
var messageView: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = .black
label.textAlignment = .left
label.numberOfLines = 0
return label
}()
var cardView: UIView = {
var cardView = UIView()
cardView.translatesAutoresizingMaskIntoConstraints = false
return UIView()
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(cardView)
cardView.addSubview(messageView)
setupCardView()
setupMessageLabel()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupCardView() {
NSLayoutConstraint.activate([
cardView.topAnchor.constraint(equalTo: self.topAnchor, constant: 5),
cardView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
cardView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10),
cardView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10)
])
cardView.layer.cornerRadius = 20.0
cardView.layer.shadowColor = #colorLiteral(red: 0.2549019754, green: 0.2745098174, blue: 0.3019607961, alpha: 1)
cardView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
cardView.layer.shadowRadius = 12.0
cardView.layer.shadowOpacity = 0.7
cardView.backgroundColor = #colorLiteral(red: 0.7490196078, green: 0.3529411765, blue: 0.9490196078, alpha: 1)
}
}
The constraints are completely ignored and I get a completely white screen
NOTE: My custom cell is wired correctly as I tried to add a label and it works properly even with constraints
The problem is solved now when I added
cardView.translatesAutoresizingMaskIntoConstraints = false
right before my constraints...
But why doesn't it work when I initialize my UIView this way
var cardView: UIView = {
var cardView = UIView()
cardView.translatesAutoresizingMaskIntoConstraints = false
return UIView()
}()
Anyone any idea?
EDIT: As you can see in the comments the problem was that I have
return UIView()
instead of
return cardView
in my cardView variable...
If I use the code sample below in a playground everything looks good until I try to set the text property for one of the labels. I've pinned it down to being able to change the value before adding the stack view to the UIView. But if I change the label's text value after adding the stack view to the parent view then the two labels end up overlapping (see images at bottom).
This is a basic test harness highlighting the problem, the real code will set the value at run time, potentially after the view has loaded, and the actual control is more complex than this. I know it's something to do with auto-layout / constraints but I'm pretty sure I've followed the example code I was looking at properly but I can't see the difference between their example and mine.
import UIKit
import PlaygroundSupport
#IBDesignable
public final class TestHarness : UIView {
fileprivate let nameLabel: UILabel = {
let label = UILabel()
//label.font = UIFont.systemFont( ofSize: 20, weight: UIFont.Weight.medium)
label.textAlignment = .center
label.textColor = #colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1)
label.text = "Person's Name"
label.adjustsFontSizeToFitWidth = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
fileprivate let jobTitleLabel: UILabel = {
let label = UILabel()
//label.font = UIFont.systemFont( ofSize: 20, weight: UIFont.Weight.medium)
label.textAlignment = .center
label.textColor = #colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1)
label.text = "Job Title"
label.adjustsFontSizeToFitWidth = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
fileprivate lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
//stackView.distribution = .fill
stackView.alignment = .center
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(self.nameLabel)
stackView.addArrangedSubview(self.jobTitleLabel)
return stackView
}()
public required init?(coder: NSCoder) {
super.init(coder: coder)
initPhase2()
}
public override init(frame: CGRect) {
super.init(frame: frame)
initPhase2()
}
private func initPhase2() {
layer.cornerRadius = 10
layer.borderWidth = 2
self.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
jobTitleLabel.backgroundColor = #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1)
// self.jobTitleLabel.text = "Developer" // <<-- Can set here no problem
self.addSubview(stackView)
// self.jobTitleLabel.text = "Developer" // << -- If I set this here, job title and name overlap
NSLayoutConstraint.activate([
{
let constraint = stackView.topAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.topAnchor, constant: 8)
constraint.priority = UILayoutPriority(750)
return constraint
}(),
stackView.leftAnchor.constraint(equalTo: layoutMarginsGuide.leftAnchor, constant: 8),
stackView.rightAnchor.constraint(equalTo: layoutMarginsGuide.rightAnchor, constant: -8),
{
let constraint = stackView.bottomAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.bottomAnchor, constant: -8)
constraint.priority = UILayoutPriority(750)
return constraint
}(),
stackView.centerYAnchor.constraint(equalTo: layoutMarginsGuide.centerYAnchor)
])
}
#IBInspectable
public var Name: String? {
get{
return self.nameLabel.text
}
set{
self.nameLabel.text = newValue
}
}
#IBInspectable
public var JobTitle: String? {
get{
return self.jobTitleLabel.text
}
set{
self.jobTitleLabel.text = newValue
}
}
}
let dimensions = (width: 200, height: 300)
let control = TestHarness(frame: CGRect(x: dimensions.width / 2, y: dimensions.height / 2, width: dimensions.width, height: dimensions.height))
// control.JobTitle = "Developer" // << -- If I set this here, job title and name overlap
let view = UIView(frame: control.frame.insetBy(dx: -100, dy: -100))
view.backgroundColor = #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1)
view.addSubview(control)
PlaygroundPage.current.liveView = view
This is how it should look, and does look if I change the label's text before adding the stack view as a child of the parent view.
This is how it looks if I change the label's text after adding the stack view to the parent view. The job title overlaps with the name label.
Your code looks good, maybe there is a problem with Playground's render loop,
I created an Xcode project and used your code and it worked great in the simulator with no overlap:
import UIKit
override func viewDidLoad() {
super.viewDidLoad()
let dimensions = (width: 200, height: 300)
let control = TestHarness(frame: CGRect(x: dimensions.width / 2, y: dimensions.height / 2, width: dimensions.width, height: dimensions.height))
control.JobTitle = "Developer b2.0" // << -- No overlap in simulator
let contentView = UIView(frame: control.frame.insetBy(dx: -100, dy: -100))
contentView.backgroundColor = #colorLiteral(red: 0.3098039329, green: 0.01568627544, blue: 0.1294117719, alpha: 1)
contentView.addSubview(control)
view.addSubview(contentView)
}