Swift to SwiftUI migration issue - ios

So I am not fully understanding how this needs to be implemented in SwiftUI for it to work properly.
I have the following code:
class RulerView: UIView {
// MARK: - Constants
private struct Constants {
static let labelWidth: CGFloat = 100
static let labelHeight: CGFloat = 20
static let labelMarginTop: CGFloat = 5
static let rulerHeight: CGFloat = 100
}
// MARK: - Properties
override var tintColor: UIColor! {
didSet { _updateStyle().setNeedsDisplay() }
}
internal let unit: Length.Unit
internal let length: CGFloat
private var lineWidth: CGFloat
private var replicatorLayer: CAReplicatorLayer?
private var labels: [UILabel]?
private var lengthUnitWidth: CGFloat {
switch unit {
case .centimeter:
return Length.pixels(fromCentimeter: 1.0)
case .inch:
return Length.pixels(fromInch: 1.0)
}
}
// MARK: - View Life Cycle
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/*
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
*/
init(unit: Length.Unit, length: CGFloat, lineWidth: CGFloat) {
self.unit = unit
self.length = length
self.lineWidth = lineWidth
super.init(frame: .zero)
let rulerWidth = lengthUnitWidth * length
frame = .init(x: 0, y: 0, width: rulerWidth, height: Constants.rulerHeight)
_setupLabels()
}
override func didMoveToWindow() {
super.didMoveToWindow()
_updateStyle()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
replicatorLayer?.removeFromSuperlayer()
replicatorLayer = CAReplicatorLayer()
if let replicatorLayer = replicatorLayer {
replicatorLayer.instanceCount = Int(ceil(length))
replicatorLayer.instanceTransform = CATransform3DMakeTranslation(lengthUnitWidth, 0, 0)
let unitLayer = LengthUnitLayer(unit: unit, lineWidth: lineWidth, lineColor: tintColor.cgColor, height: frame.height)
unitLayer.frame = CGRect(x: -lineWidth / 2, y: 0, width: unitLayer.bounds.width, height: (bounds.height - Constants.labelHeight - Constants.labelMarginTop))
unitLayer.setNeedsDisplay()
replicatorLayer.addSublayer(unitLayer)
layer.addSublayer(replicatorLayer)
}
labels?.enumerated().forEach { (offset, element) in
element.frame = .init(x: (CGFloat(offset) * lengthUnitWidth - Constants.labelWidth / 2),
y: (bounds.height - Constants.labelHeight),
width: Constants.labelWidth,
height: Constants.labelHeight)
}
}
// MARK: - Setup
#discardableResult
private func _setupLabels() -> Self {
labels?.forEach { $0.removeFromSuperview() }
labels = [UILabel]()
for i in 0...Int(ceil(length)) {
let label = UILabel()
label.text = "\(i)"
label.textAlignment = .center
addSubview(label)
labels?.append(label)
}
return self
}
// MARK: - Layout
#discardableResult
private func _updateStyle() -> Self {
labels?.forEach { $0.textColor = tintColor }
return self
}
}
And I am trying to call it in the ContentView as such:
var viewSize = UIScreen.main.bounds.size
var body: some View {
RulerView(coder: viewSize)
}
But it is not working? Does anyone know how I can resolve this?

The UIView needs to be wrapped into UIViewRepresentable to be used in SwiftUI, like below.
public struct RulerViewRep: UIViewRepresentable {
public func makeUIView(context: Context) -> RulerView {
return RulerView()
}
public func updateUIView(_ uiView: RulerView, context: Context) {
}
}
and then use it
var body: some View {
// by default consumes all space, so UIScreen.main.bounds not needed
RulerView()
}

Related

Custom iOS UIButton Sub-Class Not Updating Title

I have a subclass of a UIButton that takes the title label, and puts it under the button's image, as opposed to the right of the image:
final class ImageButton: UIButton {
#IBInspectable var cornerRadius: CGFloat = 8
#IBInspectable var borderColor: UIColor? = .black
private enum Constants {
static let imageSize: CGFloat = 40
static let titleHeight: CGFloat = 12
}
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
if #available(iOS 15, *) {
return super.titleRect(forContentRect: contentRect)
}
else {
_ = super.titleRect(forContentRect: contentRect)
return CGRect(
x: 0,
y: contentRect.height - Constants.titleHeight,
width: contentRect.width,
height: Constants.titleHeight
)
}
}
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
if #available(iOS 15, *) {
return super.imageRect(forContentRect: contentRect)
} else {
return CGRect(
x: contentRect.width / 2 - Constants.imageSize / 2,
y: (contentRect.height - titleRect(forContentRect: contentRect).height) / 2 - Constants.imageSize / 2,
width: Constants.imageSize,
height: Constants.imageSize
)
}
}
override var intrinsicContentSize: CGSize {
if #available(iOS 15, *) {
return super.intrinsicContentSize
}
else {
_ = super.intrinsicContentSize
let size = titleLabel?.sizeThatFits(contentRect(forBounds: bounds).size) ?? .zero
let spacing: CGFloat = 12
return CGSize(
width: max(size.width, Constants.imageSize),
height: Constants.imageSize + Constants.titleHeight + spacing
)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
if #available(iOS 15, *) {
var myConfiguration = UIButton.Configuration.plain()
myConfiguration.imagePlacement = .top
self.configuration = myConfiguration
} else {
titleLabel?.textAlignment = .center
}
}
override func draw(_ rect: CGRect) {
layer.cornerRadius = cornerRadius
layer.masksToBounds = true
layer.borderWidth = 1
layer.borderColor = borderColor?.cgColor
}
}
Trying to change the button's title does not have any effect:
myCustomButton.setTitle("Disable Box Select", for: .normal)
I tried adding:
myCustomButton.layer.setNeedsLayout()
myCustomButton.layer.setNeedsDisplay()
But, nothing seems to change the title of myCustomButton
This is not really an answer, but in response to the OP's comment...
Storyboard:
Complete controller code (using your posted ImageButton class, unedited):
class CustBtnVC: UIViewController {
#IBOutlet var myCustomButton: ImageButton!
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 15.0, *) {
var cfg = myCustomButton.configuration
cfg?.title = "Test"
myCustomButton.configuration = cfg
} else {
// Fallback on earlier versions
myCustomButton.setTitle("Pre-15 Test", for: .normal)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if #available(iOS 15.0, *) {
var cfg = myCustomButton.configuration
cfg?.title = "Disable Box Select"
myCustomButton.configuration = cfg
} else {
// Fallback on earlier versions
myCustomButton.setTitle("Pre-15 Disable Box Select", for: .normal)
}
}
}
Note: since your custom button code is setting the button style for iOS 15+ the button .configuration should be used instead of .setTitle(...) when running on iOS 15+
On launch:
after tap on the view:

How to change page control dot size and spacing in swift?

I want customize page control like a image.
I've already search that, but there are only deal scale.
I want change width, height, spacing.
How can I do that?
I tried this
class DefaultPageControl: UIPageControl {
override var currentPage: Int {
didSet {
updateDots()
}
}
func updateDots() {
let currentDot = subviews[currentPage]
subviews.forEach {
$0.frame.size = ($0 == currentDot) ? CGSize(width: 16, height: 4) : CGSize(width: 8, height: 4)
$0.layer.cornerRadius = 2
}
}
}
But how to change distance??
#oddK Can you try with this below answer. It's my assumption.
class DefaultPageControl: UIPageControl {
override var currentPage: Int {
didSet {
updateDots()
}
}
func updateDots() {
let currentDot = subviews[currentPage]
let spacing = 5.0
subviews.forEach {
$0.frame = ($0 == currentDot) ? CGRect(x: 0, y: 0, width: 16, height: 4) : CGRect(x: spacing, y: 0, width: 8, height: 4)
//$0.frame.size = ($0 == currentDot) ? CGSize(width: 16, height: 4) : CGSize(width: 8, height: 4)
$0.layer.cornerRadius = 2
}
}
}
The default UIPageControll is not flexible.
class ExtendedpageControll: UIView{
var numberOfPage: Int
var currentpage : Int = 0{didSet{reloadView()}}
var currentIndicatorColor: UIColor = .black
var indicatorColor: UIColor = UIColor(white: 0.9, alpha: 1)
var circleIndicator: Bool = false
private var dotView = [UIView]()
private let spacing: CGFloat = 6
private lazy var extraWidth: CGFloat = circleIndicator ? 6 : 4
init(numberOfPages: Int,currentPage: Int,isCircular: Bool){
self.numberOfPage = numberOfPages
self.currentpage = currentPage
self.circleIndicator = isCircular
super.init(frame: .zero)
configView()
}
required init?(coder: NSCoder) {fatalError("not implemented")}
private func configView(){
backgroundColor = .clear
(0..<numberOfPage).forEach { _ in
let view = UIView()
addSubview(view)
dotView.append(view)
}
}
private func reloadView(){
dotView.forEach{$0.backgroundColor = indicatorColor}
dotView[currentpage].backgroundColor = currentIndicatorColor
UIView.animate(withDuration: 0.2) {
self.dotView[self.currentpage].frame.origin.x = self.dotView[self.currentpage].frame.origin.x - self.extraWidth
self.dotView[self.currentpage].frame.size.width = self.dotView[self.currentpage].frame.size.width + (self.extraWidth * 2)
}
}
override func layoutSubviews() {
super.layoutSubviews()
for (i,view) in dotView.enumerated(){
view.clipsToBounds = true
view.layer.cornerRadius = bounds.height / 2
let width: CGFloat = circleIndicator ? self.bounds.height : CGFloat(self.bounds.width / CGFloat(self.numberOfPage) - self.spacing) - self.extraWidth
UIView.animate(withDuration: 0.2) {
view.frame = CGRect(x: ((self.bounds.width / CGFloat(self.numberOfPage)) * CGFloat(i)) + self.spacing, y: 0, width: width , height: self.bounds.height)
}
}
reloadView()
}
}
Usage: If you want to link ExtendedpageControll to a View Such as CollectionView Just Do like this: (item is your Datamodel)
class SampleViewController: UIViewController{
let colectionView = UICollectionView()
lazy var pageControll: ExtendedpageControll = {
let pc = ExtendedpageControll(numberOfPages: items.count, currentPage: 0,isCircular: true)
pc.currentIndicatorColor = .black
return pc
}()
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if pageControll.currentpage == indexPath.row {
guard let visible = self.collectionView.visibleCells.first else { return }
guard let index = self.collectionView.indexPath(for: visible)?.row else { return }
pageControll.currentpage = index
}
}
}
inside init, you can set the shape of the indicator to be circular or extended via isCircular.

UITextView IBDesignable Padding

How do I create an IBDesignable UITextView such that I can adjust the insets of the text in interface builder? I've added inspectable properties topInset, bottomInset, etc. but now I'm having trouble figure out how to actually update the insets of the UITextView such that the changes are reflected in IB
import UIKit
private let kPlaceholderTextViewInsetSpan: CGFloat = 8
#IBDesignable class UIDesignableTextView: UITextView {
// variables
#IBInspectable var topInset: CGFloat = 0.0
#IBInspectable var leftInset: CGFloat = 0.0
#IBInspectable var bottomInset: CGFloat = 0.0
#IBInspectable var rightInset: CGFloat = 0.0
var insets: UIEdgeInsets {
get {
return UIEdgeInsetsMake(topInset, leftInset, bottomInset, rightInset)
}
set {
topInset = newValue.top
leftInset = newValue.left
bottomInset = newValue.bottom
rightInset = newValue.right
}
}
#IBInspectable var placeholder: NSString? { didSet { setNeedsDisplay() } }
#IBInspectable var placeholderColor: UIColor = UIColor.lightGray
override var text: String! { didSet { setNeedsDisplay() } }
override var attributedText: NSAttributedString! { didSet { setNeedsDisplay() } }
override var contentInset: UIEdgeInsets { didSet { setNeedsDisplay() } }
override var font: UIFont? { didSet { setNeedsDisplay() } }
override var textAlignment: NSTextAlignment { didSet { setNeedsDisplay() } }
// MARK: - Lifecycle
/** Override coder init, for IB/XIB compatibility */
#if !TARGET_INTERFACE_BUILDER
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
listenForTextChangedNotifications()
}
/** Override common init, for manual allocation */
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
listenForTextChangedNotifications()
}
#endif
/** Initializes the placeholder text view, waiting for a notification of text changed */
func listenForTextChangedNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(UIDesignableTextView.textChangedForPlaceholderTextView(_:)), name:NSNotification.Name.UITextViewTextDidChange , object: self)
NotificationCenter.default.addObserver(self, selector: #selector(UIDesignableTextView.textChangedForPlaceholderTextView(_:)), name:NSNotification.Name.UITextViewTextDidBeginEditing , object: self)
}
/** willMoveToWindow will get called with a nil argument when the window is about to dissapear */
override func willMove(toWindow newWindow: UIWindow?) {
super.willMove(toWindow: newWindow)
if newWindow == nil { NotificationCenter.default.removeObserver(self) }
else { listenForTextChangedNotifications() }
}
func textChangedForPlaceholderTextView(_ notification: Notification) {
setNeedsDisplay()
setNeedsLayout()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
if text.characters.count == 0 && self.placeholder != nil {
let baseRect = placeholderBoundsContainedIn(self.bounds)
let font = self.font ?? self.typingAttributes[NSFontAttributeName] as? UIFont ?? UIFont.systemFont(ofSize: UIFont.systemFontSize)
self.placeholderColor.set()
var customParagraphStyle: NSMutableParagraphStyle!
if let defaultParagraphStyle = typingAttributes[NSParagraphStyleAttributeName] as? NSParagraphStyle {
customParagraphStyle = defaultParagraphStyle.mutableCopy() as! NSMutableParagraphStyle
} else { customParagraphStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle }
// set attributes
customParagraphStyle.lineBreakMode = NSLineBreakMode.byTruncatingTail
customParagraphStyle.alignment = self.textAlignment
let attributes = [NSFontAttributeName: font, NSParagraphStyleAttributeName: customParagraphStyle.copy() as! NSParagraphStyle, NSForegroundColorAttributeName: self.placeholderColor]
// draw in rect.
self.placeholder?.draw(in: baseRect, withAttributes: attributes)
}
}
func placeholderBoundsContainedIn(_ containerBounds: CGRect) -> CGRect {
// get the base rect with content insets.
let baseRect = UIEdgeInsetsInsetRect(containerBounds, UIEdgeInsetsMake(kPlaceholderTextViewInsetSpan, kPlaceholderTextViewInsetSpan/2.0, 0, 0))
// adjust typing and selection attributes
if let paragraphStyle = typingAttributes[NSParagraphStyleAttributeName] as? NSParagraphStyle {
baseRect.offsetBy(dx: paragraphStyle.headIndent, dy: paragraphStyle.firstLineHeadIndent)
}
return baseRect
}
This is all you need to do:
import UIKit
#IBDesignable class TextViewWithInsets: UITextView {
#IBInspectable var topInset: CGFloat = 0 {
didSet {
self.contentInset = UIEdgeInsetsMake(topInset, self.contentInset.left, self.contentInset.bottom, self.contentInset.right)
}
}
#IBInspectable var bottmInset: CGFloat = 0 {
didSet {
self.contentInset = UIEdgeInsetsMake(self.contentInset.top, self.contentInset.left, bottmInset, self.contentInset.right)
}
}
#IBInspectable var leftInset: CGFloat = 0 {
didSet {
self.contentInset = UIEdgeInsetsMake(self.contentInset.top, leftInset, self.contentInset.bottom, self.contentInset.right)
}
}
#IBInspectable var rightInset: CGFloat = 0 {
didSet {
self.contentInset = UIEdgeInsetsMake(self.contentInset.top, self.contentInset.left, self.contentInset.bottom, rightInset)
}
}
}
As you can see, these are properties of the TextViewWithInsets subclass of UITextView I have created. You need to override the didSet portion of the property method. Then, in Interface Builder these four properties (Top Inset, Bottom Inset, Left Inset, and Right Inset) will appear in the Attributes Inspector: attributes inspector in IB for new class
Just make sure that in the Identity Inspector you set the TextView object in the storyboard to be TextViewWithInsets or whatever you choose to name it like this: Set class of Text View object in storyboard to custom class
Swift 5
If you want to use it for all UITextViews in the project use the following:
import UIKit
#IBDesignable extension UITextView {
#IBInspectable var topPadding: CGFloat {
get {
return contentInset.top
}
set {
self.contentInset = UIEdgeInsets(top: newValue,
left: self.contentInset.left,
bottom: self.contentInset.bottom,
right: self.contentInset.right)
}
}
#IBInspectable var bottomPadding: CGFloat {
get {
return contentInset.bottom
}
set {
self.contentInset = UIEdgeInsets(top: self.contentInset.top,
left: self.contentInset.left,
bottom: newValue,
right: self.contentInset.right)
}
}
#IBInspectable var leftPadding: CGFloat {
get {
return contentInset.left
}
set {
self.contentInset = UIEdgeInsets(top: self.contentInset.top,
left: newValue,
bottom: self.contentInset.bottom,
right: self.contentInset.right)
}
}
#IBInspectable var rightPadding: CGFloat {
get {
return contentInset.right
}
set {
self.contentInset = UIEdgeInsets(top: self.contentInset.top,
left: self.contentInset.left,
bottom: self.contentInset.bottom,
right: newValue)
}
}
}

The right placeholder of custom UITextField in Swift 3 cannot display when I add constraints to it

I'm a beginner of Swift 3 and I plan to write several Custom Control (IBDesignable) in XCode 8. When I write a custom text field, I want to add right placeholder to it. However, the right placeholder would only display successfully when this text field has no constraint. I don't know what happened to it.
I hope someone can help me fix this bug, thanks a lot.
import UIKit
#IBDesignable
class RMLDesignableUITextField: UITextField {
// MARK: PROPERTIES
#IBInspectable var insetX: CGFloat = 0
#IBInspectable var insetY: CGFloat = 0
#IBInspectable var placeholderColor: UIColor = UIColor.white {
didSet {
if let placeholder = self.placeholder {
let attributes = [NSForegroundColorAttributeName: placeholderColor]
attributedPlaceholder = NSAttributedString(string: placeholder, attributes: attributes)
}
}
}
// MARK: Border
var bottomBorder = CALayer()
var rightBorder = CALayer()
var topBorder = CALayer()
var leftBorder = CALayer()
#IBInspectable var showsTopBorder: Bool = false {
didSet {
setupSubviews()
}
}
#IBInspectable var showsBottomBorder: Bool = false {
didSet {
setupSubviews()
}
}
#IBInspectable var showsLeftBorder: Bool = false {
didSet {
setupSubviews()
}
}
#IBInspectable var showsRightBorder: Bool = false {
didSet {
setupSubviews()
}
}
#IBInspectable var borderColor: UIColor = UIColor.clear
#IBInspectable var borderWidth: CGFloat = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var cornerRadius: CGFloat = 0 {
didSet {
layer.cornerRadius = cornerRadius
}
}
#IBInspectable var rightPlaceholder: String = "" {
didSet {
rightPlaceholderLabel.text = rightPlaceholder
}
}
fileprivate var fakePlaceholderLabel: UILabel!
fileprivate var rightPlaceholderLabel: UILabel!
fileprivate var translateX: CGFloat!
{
get {
let attributes = [NSFontAttributeName: font!]
let rightPlaceholderTextSize = rightPlaceholderLabel.text!.size(attributes: attributes)
let rightPlaceholderTextWidth = rightPlaceholderTextSize.width
let translateX = textRect(forBounds: bounds).width - rightPlaceholderTextWidth
return translateX
}
}
// MARK: Initializers
override init(frame: CGRect) {
super.init(frame: frame)
// self.setNeedsDisplay()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
// self.setNeedsDisplay()
fakePlaceholderLabel = UILabel(frame: placeholderRect(forBounds: bounds))
fakePlaceholderLabel.font = font
fakePlaceholderLabel.text = placeholder
fakePlaceholderLabel.textColor = UIColor.lightGray
fakePlaceholderLabel.alpha = 0.0
rightPlaceholderLabel = UILabel(frame: placeholderRect(forBounds: bounds))
rightPlaceholderLabel.font = font
rightPlaceholderLabel.text = rightPlaceholder
rightPlaceholderLabel.textColor = UIColor.lightGray
rightPlaceholderLabel.alpha = 0.0
}
override func layoutSubviews() {
super.layoutSubviews()
addSubview(fakePlaceholderLabel)
addSubview(rightPlaceholderLabel)
setupSubviews()
setNeedsDisplay()
}
}
// MARK: - Lifecycle
extension RMLDesignableUITextField {
override func awakeFromNib() {
super.awakeFromNib()
}
override var intrinsicContentSize : CGSize {
return CGSize(width: UIViewNoIntrinsicMetric, height: UIViewNoIntrinsicMetric)
}
override func prepareForInterfaceBuilder() {
//setupSubviews()
}
}
// MARK: - Delegate Methods
extension RMLDesignableUITextField {
// placeholder position
override func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: insetX, dy: insetY)
}
// text position
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.insetBy(dx: insetX, dy: insetY)
}
}
// MARK: - UITextField Observing
extension RMLDesignableUITextField {
override internal func willMove(toSuperview newSuperview: UIView!) {
if newSuperview != nil {
NotificationCenter.default.addObserver(self, selector: #selector(RMLDesignableUITextField.didBeginEditing(_:)), name: NSNotification.Name.UITextFieldTextDidBeginEditing, object: self)
NotificationCenter.default.addObserver(self, selector: #selector(RMLDesignableUITextField.didEndEditing(_:)), name: NSNotification.Name.UITextFieldTextDidEndEditing, object: self)
} else {
NotificationCenter.default.removeObserver(self)
}
}
func didBeginEditing(_ notification: Notification) {
placeholder = nil
if notification.object as! RMLDesignableUITextField === self{
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0, options: .curveEaseIn, animations: { () -> Void in
if self.text!.isEmpty {
self.fakePlaceholderLabel.transform = self.fakePlaceholderLabel.transform.translatedBy(x: self.translateX, y: 0.0)
self.fakePlaceholderLabel.alpha = 0.0
self.rightPlaceholderLabel.transform = self.rightPlaceholderLabel.transform.translatedBy(x: self.translateX, y: 0.0)
self.rightPlaceholderLabel.alpha = 1.0
}
}, completion: nil)
}
}
func didEndEditing(_ notification: Notification) {
if notification.object as! RMLDesignableUITextField === self {
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0, options: .curveEaseIn, animations: { () -> Void in
if self.text!.isEmpty {
self.fakePlaceholderLabel.transform = self.fakePlaceholderLabel.transform.translatedBy(x: -self.translateX, y: 0.0)
self.fakePlaceholderLabel.alpha = 1.0
self.rightPlaceholderLabel.transform = self.rightPlaceholderLabel.transform.translatedBy(x: -self.translateX, y: 0.0)
self.rightPlaceholderLabel.alpha = 0.0
}
}, completion: nil)
}
}
}
// MARK: - Methods
extension RMLDesignableUITextField {
fileprivate func pnpAddBorder(_ border: CALayer, color: UIColor, frame: CGRect) {
border.backgroundColor = color.cgColor
border.frame = frame
layer.addSublayer(border)
// layer.masksToBounds = true
}
func pnpAddTopBorder(_ width: CGFloat, color: UIColor = UIColor.black) {
pnpAddBorder(topBorder, color: color, frame: CGRect(x: 0, y: 0, width: frame.width, height: width))
}
func pnpAddBottomBorder(_ width: CGFloat, color: UIColor = UIColor.black) {
pnpAddBorder(bottomBorder, color: color, frame: CGRect(x: 0, y: frame.height - width, width: frame.width, height: width))
}
func pnpAddLeftBorder(_ width: CGFloat, color: UIColor = UIColor.black) {
pnpAddBorder(leftBorder, color: color, frame: CGRect(x: 0, y: 0, width: width, height: frame.height))
}
func pnpAddRightBorder(_ width: CGFloat, color: UIColor = UIColor.black) {
pnpAddBorder(rightBorder, color: color, frame: CGRect(x: frame.width - width, y: 0, width: width, height: frame.height))
}
func setupSubviews() {
// if (showsTopBorder || showsBottomBorder || showsLeftBorder || showsRightBorder) && borderWidth == 0 {
// borderWidth = 1
// }
if showsTopBorder {
pnpAddTopBorder(borderWidth, color: borderColor)
} else {
topBorder.removeFromSuperlayer()
}
if showsBottomBorder {
pnpAddBottomBorder(borderWidth, color: borderColor)
} else {
bottomBorder.removeFromSuperlayer()
}
if showsLeftBorder {
pnpAddLeftBorder(borderWidth, color: borderColor)
} else {
leftBorder.removeFromSuperlayer()
}
if showsRightBorder {
pnpAddRightBorder(borderWidth, color: borderColor)
} else {
rightBorder.removeFromSuperlayer()
}
self.setNeedsDisplay()
}
}

Setting the frame of a sublayer inside of layoutSubviews() makes changing it impossible. Any workaround for that?

I'm working on a customizable UITextField (see code below). I have added a border at the bottom (you can set it in the storyboard). However, I had problems setting the frame of the CALayer that this border consists of.
If I set it inside the didSet method of var showBottomBorder it doesn't appear on the screen. I think this is because the frame (of the UITextField) hasn't been calculated yet (maybe didSet gets called before that).
So I moved it to the layoutSubviews() method (see code below). This works perfectly.
But now I have another problem. I can't really change that frame anymore. Every time I change it, it gets reset by layoutSubviews() which I think is called then.
At the bottom of my code, there is the method textFieldDidBeginEditing. In there, I wanted to move up my bottom border (animated). But it doesn't work. The border does not move anywhere. And like I said, I think it's because I set the frame inside the layoutSubviews() method.
Is there a better way to set the frame of the bottom border? A way which allows me to change stuff?
#IBDesignable
class CustomizableTextField: UITextField, UITextFieldDelegate {
// MARK: - Properties
private var bottomBorder = CALayer()
// MARK: - #IBInspectables
#IBInspectable var roundCorners: CGFloat = 0 {
didSet {
self.layer.cornerRadius = roundCorners
self.clipsToBounds = true
}
}
/** -- */
#IBInspectable var borderWidth: CGFloat = 1.0 {
didSet {
self.layer.borderWidth = self.borderWidth
}
}
#IBInspectable var borderColor: UIColor = UIColor.white {
didSet {
self.layer.borderColor = self.borderColor.cgColor
}
}
/** -- */
/** -- */
private var showBottomBorder: Bool = false {
didSet {
switch showBottomBorder {
case true:
bottomBorder.borderColor = self.bottomBorderColor.cgColor
bottomBorder.borderWidth = self.bottomBorderWidth
self.layer.addSublayer(bottomBorder)
self.layer.masksToBounds = true
break
case false:
bottomBorder.removeFromSuperlayer()
break
}
}
}
#IBInspectable var bottomBorderWidth: CGFloat = 1.0 {
didSet {
self.showBottomBorder = false
self.showBottomBorder = true
}
}
#IBInspectable var bottomBorderColor: UIColor = UIColor.white {
didSet {
self.showBottomBorder = false
self.showBottomBorder = true
}
}
/** -- */
/** -- */
// Somwhow, the default panel for my font color doesn't change anything, so I created this
#IBInspectable var fixedFontColor: UIColor = UIColor.white {
didSet {
self.textColor = fixedFontColor
}
}
#IBInspectable var placeholderFontColor: UIColor = UIColor.white {
didSet {
var placeholderTxt = ""
if let txt = self.placeholder {
placeholderTxt = txt
}
self.attributedPlaceholder = NSAttributedString(string: placeholderTxt, attributes: [NSForegroundColorAttributeName: placeholderFontColor])
}
}
/** -- */
// MARK: - Overrides and Initializers
override init(frame: CGRect) {
super.init(frame: frame)
}
override func layoutSubviews() {
super.layoutSubviews()
// HERE
bottomBorder.frame = CGRect(x: 0, y: self.frame.size.height - self.bottomBorderWidth, width: self.frame.size.width, height: self.frame.size.height)
}
// setting the textField delegate to self
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//self.borderStyle = .none
self.delegate = self
}
// MARK: - Events
func textFieldDidBeginEditing(_ textField: UITextField) {
}
You can use and extension on UITextFiled for setting the border.
And keep a reference to it with KVC.
By overriding LayoutSubview, every the layout will change, we'l check if the border exists, if so remove it, and re-create a new one with the new frame:
import UIKit
let MyTopBorder = "myTopBorder"
let MyBottomBorder = "myBottomBorder"
struct Defaults {
static let width = CGFloat(1.0)
static func bottonBorderFrame(view: UIView)->CGRect {
return CGRect(x: CGFloat(0), y: view.frame.size.height - Defaults.width, width: view.frame.size.width, height: view.frame.size.height)
}
static func topBorderFrame(view: UIView)->CGRect {
return CGRect(x: CGFloat(0), y: CGFloat(0) , width: view.frame.size.width, height: Defaults.width)
}
}
extension UITextField
{
func setBottomBorder(color:CGColor)
{
if let isBottomBorder = self.getBottomBorderIfExists() {
isBottomBorder.removeFromSuperlayer()
}
self.setBorderWithFrame(Defaults.bottonBorderFrame(self), color: color, andKey: MyBottomBorder)
}
func setTopBorder(color:CGColor)
{
if let isTopBorder = self.getTopBorderIfExists() {
isTopBorder.removeFromSuperlayer()
}
self.setBorderWithFrame(Defaults.topBorderFrame(self), color: color, andKey: MyTopBorder)
}
func setBorderWithFrame(frame: CGRect, color: CGColor, andKey: String) {
self.borderStyle = UITextBorderStyle.None;
let border = CALayer()
border.borderColor = color
border.frame = frame
border.borderWidth = Defaults.width
self.layer.addSublayer(border)
self.layer.masksToBounds = true
self.layer.setValue(border, forKey: andKey)
}
func removeTopBorder() {
if let isTopBorder = self.getTopBorderIfExists() {
self.layer.setValue(nil, forKey: MyTopBorder)
isTopBorder.removeFromSuperlayer()
}
}
func removeBottomBorder() {
if let isBottomBorder = self.getBottomBorderIfExists() {
self.layer.setValue(nil, forKey: MyBottomBorder)
isBottomBorder.removeFromSuperlayer()
}
}
private func getBorderIfExistsByKey(key: String)->CALayer? {
if let isBorderSet = self.layer.valueForKey(key) {
if let borderIsCALayer = isBorderSet as? CALayer {
return borderIsCALayer
}
}
return nil
}
private func getTopBorderIfExists()->CALayer? {
return self.getBorderIfExistsByKey(MyTopBorder)
}
private func getBottomBorderIfExists()->CALayer? {
return self.getBorderIfExistsByKey(MyBottomBorder)
}
public override func layoutSubviews() {
super.layoutSubviews()
// Update bottom on frame change
if let isBottomBorder = self.getBottomBorderIfExists() {
let borderColor = isBottomBorder .borderColor
self.removeBottomBorder()
self.setBottomBorder(borderColor!)
}
// Update top on frame change
if let isTopBorder = self.getTopBorderIfExists() {
let borderColor = isTopBorder.borderColor
self.removeTopBorder()
self.setTopBorder(borderColor!)
}
}
}
Usage:
let textField = UITextField(frame: CGRect(x: 100,y: 100, width: 100, height: 100))
textField.backgroundColor = UIColor.blueColor() // Thie color is for visulizing better
self.view.addSubview(textField)
textField.setBottomBorder(UIColor.blackColor().CGColor) // Now you have a border
textField.frame = CGRect(x: 150, y: 200, width: 200, height: 200) // And the border updated to the new frame
// Now if you would like to change from bottom to top, simply do this:
textField.removeBottomBorder()
textField.setTopBorder(UIColor.blackColor().CGColor)

Resources