How can I use IBInspectable to control a property of a subview? - ios

I am writing a custom view that has two labels as subviews - titleLabel and subtitleLabel.
I added two #IBInspectable properties called titleText and subtitleText so that I can set the texts of the labels very easily in the storyboard.
class MyView : UIView {
var titleLabel: UILabel!
var subtitleLabel: UILabel!
#IBInspectable
var titleText: String? {
get { return titleLabel.text }
set {
titleLabel.text = newValue
let fontSize = // calculates appropriate font size for the text...
titleLabel.font = font.withSize(fontSize)
}
}
#IBInspectable
var subtitleText: String? {
get { return subtitleLabel.text }
set {
subtitleLabel.text = newValue
let fontSize = // calculates appropriate font size for the text...
subtitleLabel.font = font.withSize(fontSize)
}
}
override func draw(_ rect: CGRect) {
// here I make the view look prettier, irrelevant to the question
}
}
Now I need to initialise those two labels and add them as MyView's subviews. I thought I could do this in awakeFromNib:
override func awakeFromNib() {
titleLabel = UILabel(frame: CGRect(
x: ...,
y: ...,
width: ...,
height: ...))
self.addSubview(titleLabel)
subtitleLabel = UILabel(frame: CGRect(
x: ...,
y: ...,
width: ...,
height: ...))
self.addSubview(subtitleLabel)
}
So I added a MyView to the storyboard and set its properties with the properties inspector and I ran the app. It crashed.
Apparently, the IBInspectable properties are set before awakeFromNib so the labels have not been initialised by then.
This means that I need to initialise the labels in a method that is called before the IBInspectable properties are set.
What is a method that is called before IBInspectable properties are set that I can override to initialise the subviews?

This might be obvious but the correct method to put such things into is the initializer.
// called when initialized from code
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
// called when initialized from storyboard/xib
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// add subviews
}
Also, there is no real reason for optionals in your specific case:
let titleLabel: UILabel = UILabel()
You can add the labels as subviews and update frames later.

Related

Subclassed view's uiimage not appearing as IBDesignable

I have set up a subclassed UIView, and want to see the embedded image in IB - so I've set it as IBDesignable
#IBDesignable
class DieView: UIView {
#IBInspectable
var dieImage : UIImage = UIImage()
override init(frame: CGRect) {
super.init(frame: frame)
updateLayout()
}
convenience init() {
self.init(frame: CGRect.zero)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
updateLayout()
}
// for IB
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
updateLayout()
}
func updateLayout() {
self.backgroundColor = .red
let profileImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
profileImageView.image = UIImage(named: "dice1")
profileImageView.contentMode = .scaleAspectFit
profileImageView.layer.masksToBounds = false
self.addSubview(profileImageView)
}
func showNumber(number: Int) {
}
}
The background colour changes, but the embedded image doesn't update. Why not?
From the documentation of the prepareForInterfaceBuilder():
Interface Builder waits until all objects in a graph have been created
and initialized before calling this method. So if your object’s
runtime configuration relies on subviews or parent views, those
objects should exist by the time this method is called.
which says that subviews should exist before this method is called. I'm not sure, but try to add image view before this is called. Also, you have to keep in mind that prepareForInterfaceBuilder() is called independently by interface builder. Read the docs for more info. Good Luck!

A Expected Declaration Error when trying to assign a text to label

I'm trying to create a label class which contain other labels.
here is my code
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
class mainLabel: UILabel{
var top: UILabel! = UILabel()
top.text = "text" //*Expected declaration error
var down: UILabel! = UILabel()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
There are several issues with your code. The error you receive is because you can only declare variables or functions in a class scope and using top.text you're trying to modify an instance property of the class outside the function scope, which is not allowed.
Secondly, you shouldn't declare a class inside a function that rarely makes sense.
Lastly, don't declare anything as an implicitly unwrapped optional (UILabel!) if you're assigning a value to it right away.
There are several ways to create a reusable UI element that consists of 2 UILabel and can be created programatically. You can subclass a UIStackView to handle the layout automatically or if you want more control, you could simply subclass UIView, add the 2 UILabels as subViews and handle the layout by adding Autolayout constraints programatically.
Here's a solution using a UIStackView subclass. Modify any properties to fit your exact needs, this is just for demonstration.
class MainLabel: UIStackView {
let topLabel = UILabel()
let bottomLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
axis = .vertical
distribution = .fillEqually
addArrangedSubview(topLabel)
addArrangedSubview(bottomLabel)
topLabel.textColor = .orange
topLabel.backgroundColor = .white
bottomLabel.textColor = .orange
bottomLabel.backgroundColor = .white
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Test in a Playground:
PlaygroundPage.current.needsIndefiniteExecution = true
let mainLabel = MainLabel(frame: CGRect(x: 0, y: 0, width: 300, height: 200))
PlaygroundPage.current.liveView = mainLabel
mainLabel.topLabel.text = "Top"
mainLabel.bottomLabel.text = "Bottom"

iOS - UIButton become first responder to open keyboard

I need to open keyboard on button click for UIButton (not using/for UITextField). I have tried to create custom button by overriding variable canBecomeFirstResponder but it's not working.
Is there any other way to do so?
Note: I want to set UIPIckerView as an input view of UIButton in key board frame.
Here is my code.
class RespondingButton: UIButton {
override var canBecomeFirstResponder: Bool {
return true
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
// common init
}
}
In my view controller, I connected button action.
class TestViewController: UIViewController {
#IBAction func testBecomeFirstResponder(button: RespondingButton){
button.becomeFirstResponder() // Not working.
}
}
Here is what I would do.
Create transparent textField 1x1px, lets say it is myTextField.
Then add your desired button. In the button action make the myTextField.becomeFirstResponder().
Create view:
let pvBackground: UIView = {
let v = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
v.backgroundColor = .white
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
In viewDidLoad:
pvBackground.addSubview(yourPickerView)//add the picker into the pvBackground
myTextField.inputView = pvBackground
I added the pickerView into the another view to be able to customize it more.
Add conformance to UIKeyInput like this. It should work.
class RespondingButton: UIButton, UIKeyInput {
override var canBecomeFirstResponder: Bool {
return true
}
var hasText: Bool = true
func insertText(_ text: String) {}
func deleteBackward() {}
}

UITextField inside UIView doesn't size properly in XIB file when setting font size

I have a UITextField wrapped in a UIView, made into a custom class. It works fine in a ViewController (storyboard), but when used inside a UIView that is in a XIB file, the behaviour is the following:
If I set the font size to anything below 17, the expected height is 0pt.
If I set the font size to 17, expected height is correct (~20something units).
If I set the font size to anything above 17, the expected height is 4000pt.
The code in question:
import UIKit
import SnapKit
#IBDesignable class AppleBug: UIView {
// Interface builder values
#IBInspectable var placeholderText: String = "" {
didSet { updateUI() }
}
#IBInspectable var textSize: CGFloat = 14 {
didSet { updateUI() }
}
// Views
lazy var textField = UITextField()
// MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupTextField()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupTextField()
}
// MARK: View setup
private func setupTextField() {
addSubview(textField)
textField.snp_makeConstraints { (make) -> Void in
make.edges.equalTo(self)
}
}
// MARK: View updating
private func updateUI() {
textField.placeholder = placeholderText
textField.font = UIFont.systemFontOfSize(textSize)
}
}

UIView subclass in Swift: IBOutlet is found nil while unwrapping

I am trying to create a custom view in Swift by subclassing UIView, and I have a view board named MyViewPanel.xib that has its class assigned to MyCustomView. The implementation is as following:
import UIKit
#IBDesignable
class MyCustomView: UIView {
#IBOutlet weak var title: UILabel!
var question: Question {
didSet {
print("did set question, title is: \(question.title)")
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect) {
let height = rect.height
let width = rect.width
let color: UIColor = UIColor.whiteColor()
let drect = CGRect(x: (width * 0.25), y: (height * 0.25), width: (width * 0.5),height: (height * 0.5))
let bpath: UIBezierPath = UIBezierPath(rect: drect)
color.set()
bpath.stroke()
}
override func awakeFromNib() {
super.awakeFromNib()
print("awake from nib!")
self.title.text = "Test title" // error: found nil while unwrapping an Optional Value
}
}
During the run time, I encountered the following error:
fatal error: unexpectedly found nil while unwrapping an Optional value
Since the awakeFromNib() is the first lifecycle event in UIView, I do not understand why the title UILabel is nil in this case.
Edit:
To use this custom view, I just draw a UIView rectangle on my storyboard and assign its class to MyCustomView. In the viewDidLoad() method of my ViewController for the storyboard, I set the question on the custom view:
override func viewDidLoad() {
// myCustomView is an IBOutlet in the view controller
myCustomView.question = question
}
The code part looks OK but this would suggest the outlets have not been connected correctly in interface builder. In the outlets section in interface builder can you see the title outlet is connected and does not show an exclamation mark indicating an error. Also the IBOutlet line in the code should have a filled circle next to it to indicate the outlet is connected correctly and the class is correctly assigned from interface builder.
I figured out how to set the text for a UILabel in a convenience initializer for a UIView subclass I wrote:
#IBOutlet weak var messageLabel: UILabel!
init?(frame: CGRect, message: String) {
super.init(frame: frame)
guard let view = NSBundle.mainBundle().loadNibNamed("MyViewSubclass", owner: self, options: nil).first as? MyViewSubclass else {
return nil
}
// Without the following line, my view was always 600 x 600
view.frame = frame
self.addSubview(view)
guard let messageLabel = view.messageLabel else {
return
}
messageLabel.text = message
}

Resources