I am currently learning iOS and one part that I find confusing is having mandatory initializations for things like UIButtons. Here is an example below.
import UIKit
class CustomButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
init(backgroundColor: UIColor, title: String) {
super.init(frame: .zero)
self.backgroundColor = backgroundColor
setTitle(title, for: .normal)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure() {
//code that configures my button
}
}
So here I am creating a custom for my app. I have noticed I needed two inits.
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
and
init(backgroundColor: UIColor, title: String) {
super.init(frame: .zero)
self.backgroundColor = backgroundColor
setTitle(title, for: .normal)
configure()
}
I always assumed the first init was to actually initialize the button. It is the equivalent of me going to apple and saying "Hey, I want to be able to create a button based off of everything that UIButton has".
However, it seems that I have to have another init if I actually want to be able to customize the button. My second init is me pretty much saying "Hey, I want to be able to create a button with certain attributes such as a background color and a title". I find this kinda weird... Why do I need a separate initializer to do this? Why do I need to set the super.init(frame: .zero)? In a way, why do I even need a super.init in my custom init? Can't I pack everything into the first init?
Just in case, I was rambling. I find it confusing that we have to use two inits. I would think that we could just use one and pack everything into it. To me these two inits feel completely different and serve no purpose for each other. Any help would be much appreciated.
Thanks!
Related
I'm working on making a custom UIButton in Swift and have a question for initializing the UIButton with type custom.
This is the image of the current custom button in my project, and when the user taps a button, the image icon, whose the original color is .whilte, grays out. However, I want to keed the image color to white even when the user taps the button and the button state changes. I think I should initialize the button with type custom, but I get the message like, Must call a designated initializer of the superclass 'UIButton', when I try initializing with init(type: UIButton.ButtonType), so could someone point me to the right direction, please?
Here is the code, for the custom button class.
import UIKit
class MyCapsuleButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(backgroundColor: UIColor, title: String, textColor: UIColor) {
super.init(frame: .zero)
// super.init(type: .custom) -> tried to initialize with type, but didn't work
self.backgroundColor = backgroundColor
self.setTitle(title, for: .normal)
self.setTitleColor(textColor, for: .normal)
configure()
}
func configure() {
translatesAutoresizingMaskIntoConstraints = false
titleLabel?.font = UIFont.customNormal()
}
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = self.frame.height / 2
}
}
and I call as
lazy var deletionButton: MyCapsuleButton = {
let button = MyCapsuleButton(backgroundColor: .Red(), title: "DELETE", textColor: .white)
button.setImage(Images.delete, for: .normal)
return button
}()
I read the documentation and it says You specify the type of a button at creation time using the init(type:) method, I thought I need to call super.init(type: .custom) in the custom initializer, but I get a "Must call..." error on the storyboard. Also, I dont't use a storyboard in this project, and I want to know how can I call type custom with some custom init parameters, like backgroundColor, title, textColor.
Add this part later...
So, it seems when I make a subclass of UIButton, the type is gonna be custom by default. (I printed out the type and figured out.)
So is setting button.setImage(Images.delete, for: .normal) makes the trash icon gray?
When the Highlighted Adjusts Image (adjustsImageWhenHighlighted) option is enabled, button images get darker when it’s in the highlighted state.
So, You should turn off that attribute like below.
button.adjustsImageWhenHighlighted = false
OR, You can turn off in the storyboard.
Note below screen-shot.
https://i.stack.imgur.com/N42m4.png
I've created a subclass to manage my Theme but is not showing neither on device or simulator.
Here my Header.swift:
import Foundation
import UIKit
class Header: UILabel {
override var textColor: UIColor! {
// White Color
get { return ThemeManager.currentTheme.palette.primary }
set {}
}
override var font: UIFont! {
get { return ThemeManager.currentTheme.textStyle.headerText }
set {}
}
}
Here the implementation: (inside the viewcontroller)
var titleLabel: Header = Header()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
// Background Image over the view
setupBackground()
setupStartButton()
setupTitleLabel()
print(titleLabel.frame)
}
// MARK: - Header
private func setupTitleLabel() {
titleLabel.text = "0.0m"
// titleLabel.font = ThemeManager.currentTheme.textStyle.headerText
// titleLabel.textColor = ThemeManager.currentTheme.palette.primary
view.addSubview(titleLabel)
view.bringSubviewToFront(titleLabel)
setupTitleLabelAutolayout()
}
private func setupTitleLabelAutolayout() {
titleLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
But if I use UILabel instead of Header it works perfectly as expected.
I've also tried to implement init?(coder aDecoder: NSCoder) and init(frame: CGRect) but nothing changed.
If I set a frame on init then shows the text, but not styled and ignoring my constraints.
Surely I'm missing something, but what?
To avoid usefulness answers, here some infos:
The UILabel textColor is white
The background is black and has an image over it.
I've tried to remove the image and all the stuff around except for the label and nothing changed.
That's a poor reason to use subclassing. It doesn't allow you to mix-and-match when appropriate.
Better would be to make an extension:
extension UILabel {
func withHeaderStyle() -> UILabel {
self.textColor = ThemeManager.currentTheme.palette.primary
self.font = ThemeManager.currentTheme.textStyle.headerText
return self
}
}
Then at point of use:
var titleLabel = UILabel().withHeaderStyle()
You can make several of these "withXStyle" methods and at the point of use you can chain them together. That's something you can't do with inheritance.
In general you should only use inherence when you want to change behavior. It's ill suited for changing data.
I've fixed that by editing the Header to this:
import Foundation
import UIKit
class Header: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
setupStyle()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupStyle()
}
private func setupStyle() {
self.textColor = ThemeManager.currentTheme.palette.primary
self.font = ThemeManager.currentTheme.textStyle.headerText
}
}
Basically if I understood right, when I set the getter in the label it doesn't (if you think about, it's quite obvious) anything.
I still think that there are better solutions, but this works fine for me so, I'll keep that.
Now you may ask: "Why did you overwritten the getter instead of doing this?"
It's the right question, and the right answer is that I read it in a swift article on medium, so I tought it was right.
PS: I've also tried with didSet but it obviously loop through it self and crash.
I have the following code
import UIKit
class CustomButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.theme_setTitleColor(GlobalPicker.customButtonTextColor, forState: .normal)
self.theme_setTitleColor(GlobalPicker.customButtonDisabledTextColor, forState: .disabled)
self.theme_backgroundColor = GlobalPicker.primaryColor
self.layer.cornerRadius = self.frame.height/4.0
self.clipsToBounds = true
}
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = self.frame.height/2.0
self.clipsToBounds = true
}
}
And I'm getting errors when trying to build this code with Xcode 10. The code works just fine with Xcode 9 and Swift 4.0. I was hoping for a seamless transition but apparently that's not what I'm getting.
Is this an Xcode 10 bug? Anyone else running into anything similar?
My guess is that there's an extension somewhere in one of the targets of your project or workspace that messes with UIButton in a way that cripples it somehow. (The fact that this is possible is clear, and I regard it as a bug; see https://bugs.swift.org/browse/SR-2935 and the related duplicates, including https://bugs.swift.org/browse/SR-3228, and mine at https://bugs.swift.org/browse/SR-8010.)
You might be able to slide around the extension by subclassing UIKit.UIButton instead of simple UIButton. For the reason why this works, see the comment discussion in my duplicate bug report. When an extension behaves this way, it overloads methods, and you can distinguish the UIButton that doesn't have the overloads by using the module namespace.
I have a subclass of UITextView, which needs to have a specific default appearance. So far, I've been able to achieve this by overriding the initialize() class function, which has been deprecated in Swift 3.1.
public class CustomTextView : UITextView {
override public class func initialize() {
self.appearance().backgroundColor = .green
}
}
Is there a way to achieve the same thing pure Swift?
I'm working around the loss of the class method initialize by using a construct like this:
class CustomTextView : UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame:frame, textContainer: textContainer)
CustomTextView.doInitialize
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
CustomTextView.doInitialize
}
static let doInitialize : Void = {
CustomTextView.appearance().backgroundColor = .green
}()
}
This construct has the advantage that doInitialize will be initialized only once, so the code connected with it will run only once (in this case we'll configure the appearance proxy only once); and it is early enough to affect even the first instance created (that is, every CustomTextView you ever make will in fact be green).
So, as a swift novice, I'm noodling around and learning as I go. Generally a little google foo or a tutorial will help me, but now I'm stumped. So if anyone could explain this to me I would be very happy.
So I'm trying to draw a circle on screen, well, a few of them actually. I found this code online;
(http://www.ioscreator.com/tutorials/drawing-circles-uitouch-ios8-swift)
import UIKit
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawRect(rect: CGRect) {
var context = UIGraphicsGetCurrentContext();
//... various stuff to draw a circle ..//
}
}
it's a class that draws a circle when initialised like this
currentCircle = CircleView(frame: CGRectMake(0,0,100,100 ) )
I have a few questions though.
Why does it actually draw a circle? the drawRect function never gets called directly. I guess it's because we're overriding a function in UIView, and I don't understand those concepts yet.
How can a pass variables to the initialisation of that function? Say, I want to draw circles of different thickness and I want to pass an extra variable like so:
currentCircle = CircleView(frame: CGRectMake(0,0,100,100 ), thickness:10 )
How would I modify the init to accept this? adding it like this:
override init(frame: CGRec, thickness: Int) {
super.init(frame: frame)
}
triggers an error (initialiser does override a designated initialiser from its superclass)
And why all the overrides? I've tried making it a class and using the code to draw. However,
CGContextAddArc
triggers a compiler error saying the context isn't valid, so I suppose
UIGraphicsGetCurrentContext()
isn't returning anything useful.
or if anyone knows a useful resource where I can learn a bit more about overriding and initialising classes, that would be welcome.
Why does it actually draw a circle?
You're correct that you never directly call drawRect:. It's called by the system when the view needs to be redrawn. This happens when, for example, the bounds of the view change, or you call setNeedsDisplay()
How would I modify the init…
Your init should look like this:
convenience init(frame: CGRec, thickness: Int) {
self.thickness = thickness
self.init(frame: frame)
}
You can only override methods from your class's superclasses, init(frame: CGRec, thickness: Int) isn't one of them.
You should take time to read Apple's Swift documentation before embarking on tutorials. All you need to know about the language is in there.
For more info on drawing, see Defining a Custom View, here.
An elegant approach is to turn your view into a class with full Interface Builder support. That way you can directly add and configure instances of your view in Interface Builder.
To use your custom view:
Select and insert a view
In the Identity Inspector view, set the Custom Class property to your class name (CircleView)
In the Attributes Inspector view, set the thickness
There are slightly modifications to your code needed, in particular the annotations #IBDesignable and #IBInspectable:
import UIKit
#IBDesignable class CircleView: UIView {
#IBInspectable var thickness: CGFloat = 1
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawRect(rect: CGRect) {
var context = UIGraphicsGetCurrentContext()
CGContextSetLineWidth(context, thickness)
var r = self.bounds
var ellipseRect = CGRectMake(r.origin.x + self.thickness / 2, r.origin.y + self.thickness / 2,
r.size.width - thickness, r.size.height - thickness)
CGContextStrokeEllipseInRect(context, ellipseRect)
}
}
And yes, you're right. You never call 'drawRect:' yourself. You add instances of your view to your screens and iOS will call 'drawRect:' when it needs to paint your view.
It's the overall patterns of an graphical user interfaces system: you mainly react to events such as clicks from the user or repaint events.