Can't change border colour of switch inside tableview's cell - ios

I've a switch inside table which I'm creating programmatically. I can't change switch's off border colour to gray. I tried tint colour which isn't working either.
How to fix it?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? TableViewCell else {
fatalError("...")
}
//...
let switchView = UISwitch()
switchView.layer.borderColor = UIColor.greyColour.cgColor
cell.accessoryView = switchView
return cell
}

You didn't specify what effect you want to achieve but for layer.borderColor to work you need to setup layer.borderWidth also. However, because switch layer is rectangular it will look like this:
Which might be not what you want. So to make the border follow the switcher's shape you'll need to modify its corner radius:
switchView.layer.borderColor = UIColor.gray.cgColor
switchView.layer.borderWidth = 1.0
switchView.layer.cornerRadius = 16.0
to make it looks like this:
Update
If you want to apply border only for switcher off state it'll be a bit more tricky because you need to handle switcher states changes. The easiest way I could think of is to subclass UISwitch and provide your own behaviour by overriding sendActions method:
class BorderedSwitch: UISwitch {
var borderColor: UIColor = UIColor.gray {
didSet {
layer.borderColor = borderColor.cgColor
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
override var isOn: Bool {
didSet {
updateState()
}
}
override func sendActions(for controlEvents: UIControl.Event) {
super.sendActions(for: controlEvents)
if controlEvents.contains(.valueChanged) {
updateState()
}
}
private func setup() {
layer.borderColor = borderColor.cgColor
layer.cornerRadius = frame.height / 2
layer.borderWidth = 1.0
}
private func updateState() {
layer.borderWidth = isOn ? 0.0 : 1.0
}
}
Notice that I also updated cornerRadius value to frame.height / 2 to avoid magic numbers

If you add a switch with code, it looks just like a switch you add in a storyboard. Neither way of creating a switch has a border color.
The code below adds a switch to a view controller's content view:
#IBOutlet var switchView: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
switchView = UISwitch()
switchView.translatesAutoresizingMaskIntoConstraints = false //Remember to do this for UIViews you create in code
if false {
//Add a gray border to the switch
switchView.layer.borderWidth = 1.0 // Draw a rounded rect around the switchView so you can see it
switchView.layer.borderColor = UIColor.gray.cgColor
switchView.layer.cornerRadius = 16
}
//Add it to the container view
view.addSubview(switchView)
//Create center x & y layout anchors (with no offset to start)
let switchViewXAnchor = switchView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0)
switchViewXAnchor.isActive = true
let switchViewYAnchor = switchView.centerYAnchor.constraint(equalTo: view.centerYAnchor,
constant: 0.0)
switchViewYAnchor.isActive = true
}
It looks perfectly normal.

Related

Swift -Animation in GradientLayer not appearing in cell

I have different images of different foods that I add to a UIView (I choose to use an UIView instead of an UIImageView). The original color of the images are black and I change them to .lightGray using .alwaysTemplate.
// the imageWithColor function on the end turns it .lightGray: [https://stackoverflow.com/a/24545102/4833705][1]
let pizzaImage = UIImage(named: "pizzaImage")?.withRenderingMode(.alwaysTemplate).imageWithColor(color1: UIColor.lightGray)
foodImages.append(pizzaImage)
I add the food images to the UIView in cellForRow
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: foodCell, for: indexPath) as! FoodCell
cell.myView.layer.contents = foodImages[indexPath.item].cgImage
return cell
}
The UIView is inside a cell and in the cell's layoutSubviews I add a gradientLayer with an animation that gives a shimmer effect but when the cells appear on screen the animation doesn't occur.
What's the issue?
class FoodCell: UICollectionViewCell {
let myView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 7
view.layer.masksToBounds = true
view.layer.contentsGravity = CALayerContentsGravity.center
view.tintColor = .lightGray
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
setAnchors()
}
override func layoutSubviews() {
super.layoutSubviews()
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
gradientLayer.locations = [0, 0.5, 1]
gradientLayer.frame = myView.frame
let angle = 45 * CGFloat.pi / 180
gradientLayer.transform = CATransform3DMakeRotation(angle, 0, 0, 1)
let animation = CABasicAnimation(keyPath: "transform.translation.x")
animation.duration = 2
animation.fromValue = -self.frame.width
animation.toValue = self.frame.width
animation.repeatCount = .infinity
gradientLayer.add(animation, forKey: "...")
}
fileprivate func setAnchors() {
addSubview(myView)
myView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0).isActive = true
myView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0).isActive = true
myView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
myView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
}
}
I got it working.
I took #Matt's advice in the comments under the question and added myView to the cell's contentView property instead of the cell directly. I can't find the post but I just read that for animations to work in a cell, whichever views the animations are on needs to be added to the cell's contentView
I moved the gradientLayer from layoutSubviews and instead made it a lazy property.
I also moved the animation into it's own lazy property.
I used this answer and set the gradientLayer's frame to the cell's bounds property (I initially had it set to the cell's frame property)
I added a function that adds the gradientLayer to myView's layer's insertSublayer property and call that function in cellForRow. Also as per #Matt's comments under my answer to prevent the gradientLayer from constantly getting added over again I add a check to see if the gradient is in the UIView's layer's hierarchy (I got the idea from here even though it's used for a different reason). If it isn't there I add and if is I don't add it.
// I added both the animation and the gradientLayer here
func addAnimationAndGradientLayer() {
if let _ = (myView.layer.sublayers?.compactMap { $0 as? CAGradientLayer })?.first {
print("it's already in here so don't readd it")
} else {
gradientLayer.add(animation, forKey: "...") // 1. added animation
myView.layer.insertSublayer(gradientLayer, at: 0) // 2. added the gradientLayer
print("it's not in here so add it")
}
}
To call the function to add the gradientLayer to the cell it's called in cellForRow
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: foodCell, for: indexPath) as! FoodCell
cell.removeGradientLayer() // remove the gradientLayer due to going to the background and back issues
cell.myView.layer.contents = foodImages[indexPath.item].cgImage
cell.addAnimationAndGradientLayer() // I call it here
return cell
}
Updated code for the cell
class FoodCell: UICollectionViewCell {
let myView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 7
view.layer.masksToBounds = true
view.layer.contentsGravity = CALayerContentsGravity.center
view.tintColor = .lightGray
return view
}()
lazy var gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
gradientLayer.locations = [0, 0.5, 1]
gradientLayer.frame = self.bounds
let angle = 45 * CGFloat.pi / 180
gradientLayer.transform = CATransform3DMakeRotation(angle, 0, 0, 1)
return gradientLayer
}()
lazy var animation: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "transform.translation.x")
animation.duration = 2
animation.fromValue = -self.frame.width
animation.toValue = self.frame.width
animation.repeatCount = .infinity
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
return animation
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
setAnchors()
}
func addAnimationAndGradientLayer() {
// make sure the gradientLayer isn't already in myView's hierarchy before adding it
if let _ = (myView.layer.sublayers?.compactMap { $0 as? CAGradientLayer })?.first {
print("it's already in here so don't readd it")
} else {
gradientLayer.add(animation, forKey: "...") // 1. add animation
myView.layer.insertSublayer(gradientLayer, at: 0) // 2. add gradientLayer
print("it's not in here so add it")
}
}
// this function is explained at the bottom of my answer and is necessary if you want the animation to not pause when coming from the background
func removeGradientLayer() {
myView.layer.sublayers?.removeAll()
gradientLayer.removeFromSuperlayer()
setNeedsDisplay() // these 2 might not be necessary but i called them anyway
layoutIfNeeded()
if let _ = (iconImageView.layer.sublayers?.compactMap { $0 as? CAGradientLayer })?.first {
print("no man the gradientLayer is not removed")
} else {
print("yay the gradientLayer is removed")
}
}
fileprivate func setAnchors() {
self.contentView.addSubview(myView)
myView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0).isActive = true
myView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0).isActive = true
myView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0).isActive = true
myView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0).isActive = true
}
}
As a side note this BELOW works great if the users CAN’T scroll the cells (placeholder cells) but if they CAN make sure to test before adding because it's buggy
Another problem I ran into was when I would go to the background and come back the animation wouldn't move. I followed this answer (code below on how to use it) which works although in that same thread I amended that answer to use this answer to start the animation from the beginning which works BUT there are issues.
I noticed even though I came back from the foreground and the animation worked sometimes when I scrolled the animation got stuck. To get around it I called cell.removeGradientLayer() in cellForRow and then again as explained below. However it still got stuck when scrolling but by calling the above it got unstuck. It works for what I need it for because I only show these cells while the actual cells are loading. I'm disabling scrolling when the animation occurs anyway so I don't have to worry about it. FYI this stuck issue only seems to happen when coming back from the background and then scrolling.
I also had to remove the gradientLayer from the cell by calling cell.removeGradientLayer() when the app went to the background and then when it came back to the foreground I had to call cell.addAnimationAndGradientLayer() to add it again. I did that by adding background/foreground Notifications in the class that has the collectionView. In the accompanying Notification functions I just scroll through the visible cells and call the cell's functions that are necessary (code is also below).
class PersistAnimationView: UIView {
private var persistentAnimations: [String: CAAnimation] = [:]
private var persistentSpeed: Float = 0.0
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
func commonInit() {
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.willEnterForegroundNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func didBecomeActive() {
self.restoreAnimations(withKeys: Array(self.persistentAnimations.keys))
self.persistentAnimations.removeAll()
if self.persistentSpeed == 1.0 { //if layer was plaiyng before backgorund, resume it
self.layer.resume()
}
}
func willResignActive() {
self.persistentSpeed = self.layer.speed
self.layer.speed = 1.0 //in case layer was paused from outside, set speed to 1.0 to get all animations
self.persistAnimations(withKeys: self.layer.animationKeys())
self.layer.speed = self.persistentSpeed //restore original speed
self.layer.pause()
}
func persistAnimations(withKeys: [String]?) {
withKeys?.forEach({ (key) in
if let animation = self.layer.animation(forKey: key) {
self.persistentAnimations[key] = animation
}
})
}
func restoreAnimations(withKeys: [String]?) {
withKeys?.forEach { key in
if let persistentAnimation = self.persistentAnimations[key] {
self.layer.add(persistentAnimation, forKey: key)
}
}
}
}
extension CALayer {
func pause() {
if self.isPaused() == false {
let pausedTime: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil)
self.speed = 0.0
self.timeOffset = pausedTime
}
}
func isPaused() -> Bool {
return self.speed == 0.0
}
func resume() {
let pausedTime: CFTimeInterval = self.timeOffset
self.speed = 1.0
self.timeOffset = 0.0
self.beginTime = 0.0
// as per the amended answer comment these 2 lines out to start the animation from the beginning when coming back from the background
// let timeSincePause: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
// self.beginTime = timeSincePause
}
}
And in the cell class instead of making MyView and instance of UIView I instead made it an instance of PersistAnimationView like this:
class FoodCell: UICollectionViewCell {
let MyView: PersistAnimationView = {
let persistAnimationView = PersistAnimationView()
persistAnimationView.translatesAutoresizingMaskIntoConstraints = false
persistAnimationView.layer.cornerRadius = 7
persistAnimationView.layer.masksToBounds = true
persistAnimationView.layer.contentsGravity = CALayerContentsGravity.center
persistAnimationView.tintColor = .lightGray
return persistAnimationView
}()
// everything else in the cell class is the same
Here are the Notifications for the class with the collectionView. The animations also stop when the view disappears or reappears so you’ll have to manage this in viewWillAppear and viewDidDisappear too.
class MyClass: UIViewController, UICollectionViewDatasource, UICollectionViewDelegateFlowLayout {
var collectionView: UICollectionView!
// MARK:- View Controller Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(appHasEnteredBackground), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
addAnimationAndGradientLayerInFoodCell()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
removeGradientLayerInFoodCell()
}
// MARK:- Functions for Notifications
#objc func appHasEnteredBackground() {
removeGradientLayerInFoodCell()
}
#objc func appWillEnterForeground() {
addAnimationAndGradientLayerInFoodCell()
}
// MARK:- Supporting Functions
func removeGradientLayerInFoodCell() {
// if you are using a tabBar, switch tabs, then go to the background, comeback, then switch back to this tab, without this check the animation will get stuck
if (self.view.window != nil) {
collectionView.visibleCells.forEach { (cell) in
if let cell = cell as? FoodCell {
cell.removeGradientLayer()
}
}
}
}
func addAnimationAndGradientLayerInFoodCell() {
// if you are using a tabBar, switch tabs, then go to the background, comeback, then switch back to this tab, without this check the animation will get stuck
if (self.view.window != nil) {
collectionView.visibleCells.forEach { (cell) in
if let cell = cell as? FoodCell {
cell.addAnimationAndGradientLayer()
}
}
}
}
}
You could maybe try this, put this code inside it's own function:
func setUpGradient() {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
...
gradientLayer.add(animation, forKey: "...")
}
Then in your init function call it
override init(frame: CGRect) {
super.init(frame: frame)
setUpGradient()
}
It seems like your problem might be the layoutSubviews can be called a lot, but the init function will only be called when the view is initialized with a frame. Also putting the setup code in its own function will make it easier to do other things like update the gradient layer's frame if the frame changes.

How to achieve an angular butterbar effect in swift?

We are looking to achieve something like a "butterbar", which is basically a UIView that changes color from the middle outwards to the edges. An example is this codepen .
How to do this in swift?
Here's a solution for you. Basically, there's a ButterBar UIView subclass, that has an inner view subview, and an array of colours.
Set the background to colour[0]
Set the inner background to colour[1]
Set the inner width to 0
Animate the inner width to the ButterBar width
Set the background colour to the inner background colour
Set the inner background to the next colour
Repeat
Code…
class ButterBar: UIView {
private let innerView = UIView(frame: .zero)
private var colours: [UIColor] = [.black, .white]
private var colourIndex = 0
private var isAnimating = false
private lazy var widthConstraint: NSLayoutConstraint = {
return innerView.widthAnchor.constraint(equalToConstant: 0)
}()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
func configure(colours: [UIColor]) {
guard colours.count > 1 else { return }
self.colours = colours
}
func startAnimating() {
colourIndex = 0
isAnimating = true
updateColours()
animate()
}
func stopAnimating() {
isAnimating = false
}
}
private extension ButterBar {
func configure() {
innerView.translatesAutoresizingMaskIntoConstraints = false
addSubview(innerView)
topAnchor.constraint(equalTo: innerView.topAnchor).isActive = true
bottomAnchor.constraint(equalTo: innerView.bottomAnchor).isActive = true
centerXAnchor.constraint(equalTo: innerView.centerXAnchor).isActive = true
widthConstraint.isActive = true
}
func updateColours() {
backgroundColor = colours[colourIndex]
colourIndex = (colourIndex + 1) % colours.count
innerView.backgroundColor = colours[colourIndex]
}
func animate() {
widthConstraint.constant = 0
layoutIfNeeded()
widthConstraint.constant = bounds.width
UIView.animate(withDuration: 1, animations: {
self.layoutIfNeeded()
}) { _ in
if self.isAnimating {
self.updateColours()
self.animate()
}
}
}
}
And…
#IBOutlet weak var butterBar: ButterBar!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
butterBar.configure(colours: [.red, .blue, .green, .yellow])
butterBar.startAnimating()
}
You could use two UIViews that are constrained to the center of the parent and possess width constraints. Simply set the background color of the first view and animate its width constraint from 0 to the width of the parent. Once this is done, you can bring the other view to front and animate its width from 0 to the width of the parent, then restart with the initial view to keep the cycle going.

How do I animate the height of an input accessory view?

I am experiencing strange behavior when animating the height of an input accessory view. What am I doing wrong?
I create a UIInputView subclass (InputView) with a single subview. The height of InputView and its intrinsicContentSize are controlled by the subview. InputView is 50 pixels tall when isVisible is true and 0 pixels tall when isVisible is false.
import UIKit
class InputView: UIInputView {
private let someHeight: CGFloat = 50.0, zeroHeight: CGFloat = 0.0
private let subView = UIView()
private var hide: NSLayoutConstraint?, show: NSLayoutConstraint?
var isVisible: Bool {
get {
return show!.isActive
}
set {
// Always deactivate constraints before activating conflicting ones
if newValue == true {
hide?.isActive = false
show?.isActive = true
} else {
show?.isActive = false
hide?.isActive = true
}
}
}
// MARK: Sizing
override func sizeThatFits(_ size: CGSize) -> CGSize {
return CGSize(width: size.width, height: someHeight)
}
override var intrinsicContentSize: CGSize {
return CGSize.init(width: bounds.size.width, height: subView.bounds.size.height)
}
// MARK: Initializers
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect, inputViewStyle: UIInputViewStyle) {
super.init(frame: frame, inputViewStyle: inputViewStyle)
addSubview(subView)
subView.backgroundColor = UIColor.purple
translatesAutoresizingMaskIntoConstraints = false
subView.translatesAutoresizingMaskIntoConstraints = false
subView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor).isActive = true
subView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
subView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
subView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor).isActive = true
show = subView.heightAnchor.constraint(equalToConstant: someHeight)
hide = subView.heightAnchor.constraint(equalToConstant: zeroHeight)
hide?.isActive = true
}
}
The host view controller toggles isVisible in a one-second animation block when a button is pressed.
import UIKit
class MainViewController: UIViewController {
let testInputView = InputView.init(frame: .zero, inputViewStyle: .default)
#IBAction func button(_ sender: AnyObject) {
UIView.animate(withDuration: 1.0) {
let isVisible = self.testInputView.isVisible
self.testInputView.isVisible = !isVisible
self.testInputView.layoutIfNeeded()
}
}
override var canBecomeFirstResponder: Bool {
return true
}
override var inputAccessoryView: UIView? {
return testInputView
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
I expect the input accessory view to smoothly grow from the botton of the screen when isVisible is set to true, and smoothly shrink to the button of the screen when isVisible is set to false. Instead, the keyboard background overlay appears at full 50-pixel height as soon as isVisible is true and the input accessory view grows from the center of its frame.
When shrinking, the input accessory view instantly loses some of its height before continuing the animation smoothly.
I created an input accessory view demonstration project that displays this unexpected behavior.
This will give you the correct animation:
UIView.animate(withDuration: 1.0) {
let isVisible = self.testInputView.isVisible
self.testInputView.isVisible = !isVisible
self.testInputView.superview?.superview?.layoutIfNeeded()
}
However, it's never a good practice to call superview if Apple changes the design. So there may be a better answer.
This is what the superviews represent:
print(testInputView.superview) // UIInputSetHostView
print(testInputView.superview?.superview) // UIInputSetContainerView
EDIT: ADDED A SAFER SOLUTION
I'm not too familiar with the UIInputView. But one way of solving it without calling the superview would be to only animate the height change of the subview:
Step 1:
Move the isVisible outside the animation block.
#IBAction func button(_ sender: AnyObject) {
let isVisible = self.testInputView.isVisible
self.testInputView.isVisible = !isVisible
UIView.animate(withDuration: 1.0) {
self.testInputView.layoutIfNeeded()
}
}
Step 2:
Create a new method in your InputView which changes the height constraint of the InputView instead of the intrinsicContentSize.
private func updateHeightConstraint(height: CGFloat) {
for constraint in constraints {
if constraint.firstAttribute == .height {
constraint.constant = height
}
}
self.layoutIfNeeded()
}
Step 3:
And call that method inside the setter.
if newValue == true {
updateHeightConstraint(height: someHeight)
hide?.isActive = false
show?.isActive = true
} else {
updateHeightConstraint(height: zeroHeight)
show?.isActive = false
hide?.isActive = true
}
Step 4:
Lastly some changes in the init.
override init(frame: CGRect, inputViewStyle: UIInputViewStyle) {
super.init(frame: frame, inputViewStyle: inputViewStyle)
addSubview(subView)
backgroundColor = .clear
subView.backgroundColor = UIColor.purple
subView.translatesAutoresizingMaskIntoConstraints = false
subView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
subView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
subView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor).isActive = true
show = subView.heightAnchor.constraint(equalToConstant: someHeight)
hide = subView.heightAnchor.constraint(equalToConstant: zeroHeight)
hide?.isActive = true
}
Conclusion:
This result in the InputView changes it's height before animating the height of the purple subview. The only downside is the UIInputView, which has some kind of gray background as default and cannot be changed to Clear. However, you can use the same backgroundColor as the VC.
But if you instead should go with a regular UIView as InputAccessoryView it will be UIColor.clear as default. Than the first "jump" will not be noticed.

How to create IBDesignable custom view?

I am still improving my programing skills in iOS(Swift). Today I am thinking about creating a UIView subclasses in a good way. I want to create custom view which is:
#IBDesignable - I want to see a render of my view in storyboard/xib;
Creatable from code
I know such methods as init(withFrame) init() init(withDecoder) prepareForInterfacebuilder(), etc but I don't know how to use them to minimize redundancy of code.
For example here is my custom UIButton:
import UIKit
#IBDesignable
class SecurityButton: UIButton {
#IBInspectable
var color: UIColor = UIColor.black {
didSet {
setTitleColor(color, for: .normal)
contentEdgeInsets = UIEdgeInsetsMake(0.0, 8.0, 0.0, 8.0)
layer.borderWidth = 1.0
layer.borderColor = color.cgColor
layer.cornerRadius = 6.0
}
}
}
It works good but I know that contentEdgeInsets = UIEdgeInsetsMake(0.0, 8.0, 0.0, 8.0) layer.cornerRadius = 6.0 layer.borderWidth = 1.0 are called every I set a new color. Where should I place this custom setup to keep my requirements about custom view?
Edit
I do not looking for fix my example. I looking for a good place to initialize my custom view. Suppose that you need to do some time-consuming operations but only ones on create view.
You are missing the function
self.setNeedsDisplay()
Your code should look as follows
import UIKit
#IBDesignable
class SecurityButton: UIButton {
#IBInspectable
var color: UIColor = UIColor.black {
didSet {
setTitleColor(color, for: .normal)
contentEdgeInsets = UIEdgeInsetsMake(0.0, 8.0, 0.0, 8.0)
layer.borderWidth = 1.0
layer.borderColor = color.cgColor
layer.cornerRadius = 6.0
self.setNeedsDisplay()
}
}
}
This will call the draw function that will update the storyboard view.
The function should be called as last in the didSet Now it will show up after you set the color.
I tested it and It works for me in xcode, after I set the value color in the storyboard.
If you want it to draw always not only when u set a color by the inspector you can use the draw(rect) function it will look following
import UIKit
#IBDesignable
class SecurityButton: UIButton {
#IBInspectable
var color: UIColor = UIColor.black
override func draw(_ rect: CGRect) {
setTitleColor(color, for: .normal)
contentEdgeInsets = UIEdgeInsetsMake(0.0, 8.0, 0.0, 8.0)
layer.borderWidth = 1.0
layer.borderColor = color.cgColor
layer.cornerRadius = 6.0
setNeedsDisplay()
}
}
This will draw the border always, not only when you set in in the interface inspector.
You can create a "custom init" function to handle the setup tasks.
So, for example:
import UIKit
#IBDesignable
class SecurityButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override func awakeFromNib() {
super.awakeFromNib()
commonInit()
}
func commonInit() {
layer.borderWidth = 1.0
layer.borderColor = color.cgColor
layer.cornerRadius = 6.0
_contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 8.0)
updateEdgeInsets()
}
private var _contentEdgeInsets: UIEdgeInsets = UIEdgeInsets.zero {
didSet { updateEdgeInsets() }
}
override public var contentEdgeInsets: UIEdgeInsets {
get { return super.contentEdgeInsets }
set { _contentEdgeInsets = newValue }
}
private func updateEdgeInsets() {
let initialEdgeInsets = contentEdgeInsets
let t = _contentEdgeInsets.top + initialEdgeInsets.top
let l = _contentEdgeInsets.left + initialEdgeInsets.left
let b = _contentEdgeInsets.bottom + initialEdgeInsets.bottom
let r = _contentEdgeInsets.right + initialEdgeInsets.right
super.contentEdgeInsets = UIEdgeInsets(top: t, left: l, bottom: b, right: r)
}
#IBInspectable
var color: UIColor = UIColor.black {
didSet {
setTitleColor(color, for: .normal)
}
}
}
Edit: Custom contentEdgeInsets have to be handled a little differently than most other properties.
#KamilHarasimowicz - Your "question edit" states "I do not looking for fix my example. I looking for a good place to initialize my custom view." ...
My initial answer did exactly that - it showed you where to initialize your custom view.
However, since your comment "storyboard don't render contentEdgeInsets in your solution" indicates that you actually DO want your example fixed (otherwise you would have just fixed it yourself), I have edited my answer to do so.

setting background highlight on custom button layer

I am working on an app where I have a custom button class (I found this class here on StackOverflow - credit to the original poster). where I have added a CAShapelayer to the button to give it a shape like the rounded rectangular button. This works well, but I would like to highlight the button when the user holds their finger on it, and I am running into a problem. When I override the highlighted color, it only highlights the layer BEHIND my rounded rectangle layer.
Can someone help me understand what I need to do to make my custom button highlight properly ? I am building this in Swift, in iOS8.1. Thank you much!
Custom button code:
public var roundRectCornerRadius: CGFloat = 8 {
didSet {
self.setNeedsLayout()
}
}
/// background of the rounded lyer
public var roundRectColor: UIColor = UIColor.whiteColor() {
didSet {
self.setNeedsLayout()
}
}
override public func layoutSubviews() {
super.layoutSubviews()
layoutRoundRectLayer()
}
// how do i get this into the roundrectlayer?
override public var highlighted: Bool {
get {
return super.highlighted
}
set {
if newValue {
backgroundColor = UIColor.blackColor()
}
else {
backgroundColor = UIColor.whiteColor()
}
super.highlighted = newValue
}
}
private var roundRectLayer: CAShapeLayer?
private func layoutRoundRectLayer() {
if let existingLayer = roundRectLayer {
existingLayer.removeFromSuperlayer()
}
let shapeLayer = CAShapeLayer()
shapeLayer.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: roundRectCornerRadius).CGPath
shapeLayer.fillColor = roundRectColor.CGColor
self.layer.insertSublayer(shapeLayer, atIndex: 0)
self.roundRectLayer = shapeLayer
}

Resources