Common init when using constants - ios

Utilizing constants in subclasses with lot of initializers to override is tedious. Look at the class below, I need to duplicate the code in both initializers.
class Test : UIView {
let subview: UIView
override init(frame: CGRect) {
subview = UIView() // once
super.init(frame: frame)
}
required init(coder aDecoder: NSCoder) {
subview = UIView() // twice
super.init(coder: aDecoder)
}
}
If I try to make use of a common initializer then I get the following errors (see the comments)
override init(frame: CGRect) {
commonInit() // 1: Use of 'self' in method call 'commonInit' before super.init initializes self
super.init(frame: frame) // 2: Property 'self.subview' is not in initialized at super.init call
}
private func commonInit() {
subview = UIView() // 3: Cannot assign to 'subview' in 'self'
}
It works fine if I do not use a constant and define the subview like:
var subview: UIView?
And then of course switch order in init like this:
super.init(frame: frame)
commonInit()
So my question: is there no way to use a common initializer for constants in Swift as of now?
EDIT: I totally forgot to mention that the struggle here is that I can't initiate the subview before I'm in the init, it's initiated based on data that is not known when declaring the constant.

Try this:
class Test : UIView {
let subview = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}

Another option:
class Test : UIView {
let subview:UIView
init(frame: CGRect?, coder: NSCoder?) {
// The first phase initialization here
subview = UIView()
if let frame = frame {
super.init(frame: frame)
}
else if let coder = coder {
super.init(coder: coder)
}
else {
super.init()
}
// the Second phase initialization here
self.addSubview(subview)
}
convenience init() {
self.init(frame: nil, coder: nil)
}
override convenience init(frame: CGRect) {
self.init(frame: frame, coder: nil)
}
required convenience init(coder aDecoder: NSCoder) {
self.init(frame: nil, coder: aDecoder)
}
}
A little bit cleaner alternative:
class Test : UIView {
let subview:UIView
private enum SuperInitArg {
case Frame(CGRect), Coder(NSCoder), None
}
private init(_ arg: SuperInitArg) {
subview = UIView()
switch arg {
case .Frame(let frame): super.init(frame:frame)
case .Coder(let coder): super.init(coder:coder)
case .None: super.init()
}
addSubview(subview)
}
convenience init() {
self.init(.None)
}
override convenience init(frame: CGRect) {
self.init(.Frame(frame))
}
required convenience init(coder aDecoder: NSCoder) {
self.init(.Coder(aDecoder))
}
}

Do the following:
class Test : UIView {
let subview = UIView()
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//edit subview properties as needed
}
}

This works well for me:
// Declare this somewhere (it can be used by multiple classes)
class FrameCoder: NSCoder {
let frame: CGRect
init(_ frame: CGRect) {
self.frame = frame
super.init()
}
}
Then, when you want a common initializer pattern, use this:
class MyView: UIView {
let something: SomeType
required init(coder aDecoder: NSCoder) {
if (aDecoder is FrameCoder) {
super.init(frame: (aDecoder as! FrameCoder).frame)
}
else {
super.init(coder: aDecoder)
}
// Common initializer code goes here...
something = // some value
}
override init(frame: CGRect) {
self.init(coder: FrameCoder(frame))
}
}
The advantage of using this method is you don't need to create default values for let definitions -- you can set them to the correct values in context, just like you would if there were only a single initializer.
Note that you can use this technique for initializers taking arbitrary values (not just for init(frame: CGRect)) -- you can create a specific NSCoder subclass to wrap any value and type you need to pass to an initializer, and then chain it into your init(coder:) method.
(also, there's probably some way to do this with a generic... haven't quite figured that out yet! Anyone...?)

One option is following the Xcode pattern:
class Test : UIView {
var subview: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
subview = UIView()
}
}
Notice your subview is a var
Another option is:
class Test : UIView {
let subview: UIView = {
let sv = UIView()
// some config, (i.e.: bgColor etc., frame is not yet _real_
// can't yet access instance's frame and other properties
return sv
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
// frame might be valid, preferably use layout constraints
addSubview(subview)
}
}
Hope this helps

Related

swift Property not initialized at super.init call

I have a class called badge that inherits from another class and I want the constructor method to tell the style of the notification.
but I'm having this error:
Property 'self.notificationStyle' not initialized at super.init call
DefaultTableViewCell class
final public class DefaultTableViewCell: UITableViewCell
enum NotificationStyle {
case numberedSquare, circle
}
var notificationStyle: NotificationStyle = .numberedSquare
my goal is whenever someone instantiates this Badge class, it will be necessary to inform the notificationStyle of it, being square or circular in this case.
how can i solve this?
Badge class
#objc public class Badge: NotifyLabel
var notificationStyle: DefaultTableViewCell.NotificationStyle
init(frame: CGRect, notificationStyle: DefaultTableViewCell.NotificationStyle) {
self.notificationStyle = notificationStyle
super.init(frame: frame)
setup(notificationStyle: notificationStyle)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup(notificationStyle: notificationStyle)
}
init(notificationStyle: DefaultTableViewCell.NotificationStyle) {
self.notificationStyle = notificationStyle
super.init(frame: .zero)
setup(notificationStyle: notificationStyle)
}
func configureBadgeNotificationStyle(notificationStyle: DefaultTableViewCell.NotificationStyle) {
textAlignment = NSTextAlignment.center
layer.borderWidth = 1
layer.borderColor = Color.bostonRed.cgColor
clipsToBounds = true
textStyle = .label
backgroundColor = Color.white
switch notificationStyle {
case .circle:
layer.cornerRadius = 8
default:
layer.cornerRadius = 2
}
}
private func setup(notificationStyle: DefaultTableViewCell.NotificationStyle) {
configureBadgeNotificationStyle(notificationStyle: notificationStyle)
configureAccessibility()
}
NotifyLabel class
public class NotifyLabel: UILabel
public init(textStyle: TextStyle) {
self.textStyle = textStyle
super.init(frame: .zero)
applyTextStyle()
}
public override init(frame: CGRect) {
super.init(frame: frame)
applyTextStyle()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
applyTextStyle()
}
You can fix this by adding a line to set notificationStyle to a default value in init?(coder aDecoder: NSCoder):
required init?(coder aDecoder: NSCoder) {
self.notificationStyle = .numberedSquare //<-- Here
super.init(coder: aDecoder)
setup(notificationStyle: notificationStyle)
}
You have to do this because in your declaration of notificationStyle, there's no default value and it must have a value before calling super.init. In your other initializers, you set it based on the incoming arguments.
This is an initializer that it sounds like you're not using anyway, but it is required with UIViews that we implement this required initializer.

Force user to use custom init method for the initialization

I am creating a UIView subclass with the intention to force users to my required init method than the default one.
So for that, I have created a convenience method for this.
#available(*, unavailable, message: "init is unavailable.")
public override init(frame: CGRect) {
super.init(frame: frame)
}
required
convenience public init(withSomeParameters myParam:Type) {
self.init(frame: CGRect.zero)
//Doing something nice!
}
This works! However, when I try to init it's showing me two ways to initialize it. How to force the user to make use of custom init method?
You can make it private , so user must need to init Test class with withSomeParameters
class Test:UIView {
private override init(frame: CGRect) {
super.init(frame: frame)
}
convenience public init(withSomeParameters myParam:Type) {
self.init(frame: CGRect.zero)
//Doing something nice!
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Maybe you need to mark the coder initialiser as unavailable as well:
#available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
Plus: remove the convenience from your initialiser, and call supers init(frame:)
public init(withSomeParameters myParam:Type) {
super.init(frame: .zero)
//Doing something nice!
}
As another example, here is some base UIView subclass I use in a lot of my projects that don't utilise storyboards:
class MXView: UIView {
init() {
super.init(frame: .zero)
}
// Storyboards are incompatible with truth and beauty.
#available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Subclass:
class CustomView: MXView {
init(someParameters params: Type) {
// Phase 1: store ivars.
super.init()
// Phase 2: Do something nice.
}
If you do it like that, users of CustomView will be forced to use init(someParamters:). init(frame:) is shadowed because init(someParameters:) is a non-convenience init.

Why can't I use "self.init(type: .custom)" in convenience initializer at my subclass of UIButton

I know must call a designated initializer of the superclass, I think init(type: UIButtonType) had called a designated initializer, so I used it in subclass convenience initializer, but failed
class TSContourButton: UIButton {
enum ContourButtonSizeType {
case large
case small
}
convenience init(type:ContourButtonSizeType) {
self.init(type: .custom)
}
then, I tried this. It compiles okay. but, It doesn't look professional
class TSClass: UIButton {
convenience init(frame: CGRect, myString: String) {
self.init(frame: frame)
self.init(type: .custom)
}
so, I doubt that I may think wrong. So, I did some test. It successfully called super convenience initializer. Why I can't use self.init(type: .custom) in convenience initializer at my subclass of UIButton?
class person: UIButton {
var name: String = "test"
override init(frame: CGRect) {
super.init(frame: .zero)
self.name = "one"
}
convenience init(myName: String) {
self.init(frame: .zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class man: person {
convenience init(mySex: Int) { // it successfully call superclass convenience initializer
self.init(myName: "info")
}
If, for say, name is your mandatory field, you implement all your initial set up in a function. And you should handle if name is not available. I'll keep small as the default option if no type was provided.
// MARK:- Designated Initializers
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialSetup(type: .small)
}
override init(frame: CGRect) {
super.init(frame: frame)
initialSetup(type: .small)
}
// MARK:- Convenience Initializers
convenience init(type: ContourButtonSizeType) {
self.init(frame: .zero)
initialSetup(type: type)
}
func initialSetup(type: ContourButtonSizeType) {
// handle all initial setup
}
init(type: UIButtonType) is not UIButton's designated initializer, init(frame: CGRect) is the right designated initializer of UIButton
you just need to overwrite init(frame: CGRect)
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button1 = MyButton(type: .custom)
}
}
class MyButton: UIButton {
// 初始化父类的指定构造器,然后你就可以获得父类的便利构造器
// overwrite the designated initializer of the super class, then you can automatically inherit the convenience initializer of the super class
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
if you want to customize your own button, you can add convenience init(myType: UIButton.ButtonType)
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button1 = MyButton(type: .custom)
let button2 = MyButton(myType: .custom)
}
}
class MyButton: UIButton {
// 初始化父类的指定构造器,然后你就可以获得父类的便利构造器
// overwrite the designated initializer of the super class, then you can automatically inherit the convenience initializer of the super class
override init(frame: CGRect) {
super.init(frame: frame)
}
// 创建自己的遍历构造器
// this is what you want
convenience init(myType: UIButton.ButtonType) {
self.init(type: myType) // called the convenience initializer which you automatically inherit from super class
// customize your own button
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If this is useful to you, can you give me a like, (づ ̄ 3 ̄)づ

Pass variables into drawRect in swift

I want to pass array of colors into drawRect in swift, how can I do that? (I'm getting alot of errors..)
class GradientColorView : UIView {
static let colors : NSArray = NSArray()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
class func initWithColors(colors :NSArray) {
}
override func drawRect(rect: CGRect) {
println(self.colors)
println("drawRect has updated the view")
}
}
Your class has color as static variable which is just like a class variable and it is let which means that is is immutable constant. You would need to change that let to var if you want it to be modifiable. So, you cannot access that from instance. I would propose you to change that to instance variable which makes it easy to do drawing call when colors changes.
You could do something like this,
class GradientColorView : UIView {
var colors : NSArray = NSArray() {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
class func initWithColors(colors :NSArray) {
}
override func drawRect(rect: CGRect) {
println(self.colors)
println("drawRect has updated the view")
}
}
Then you can update the colors from the instance of gradientView which will redraw it again,
let gradientView = GradientColorView(frame: CGRectMake(0, 0, 200, 200))
gradientView.colors = [UIColor.redColor(), UIColor.orangeColor(), UIColor.purpleColor()]

How do I write a custom init for a UIView subclass in Swift?

Say I want to init a UIView subclass with a String and an Int.
How would I do this in Swift if I'm just subclassing UIView? If I just make a custom init() function but the parameters are a String and an Int, it tells me that "super.init() isn't called before returning from initializer".
And if I call super.init() I'm told I must use a designated initializer. What should I be using there? The frame version? The coder version? Both? Why?
The init(frame:) version is the default initializer. You must call it only after initializing your instance variables. If this view is being reconstituted from a Nib then your custom initializer will not be called, and instead the init?(coder:) version will be called. Since Swift now requires an implementation of the required init?(coder:), I have updated the example below and changed the let variable declarations to var and optional. In this case, you would initialize them in awakeFromNib() or at some later time.
class TestView : UIView {
var s: String?
var i: Int?
init(s: String, i: Int) {
self.s = s
self.i = i
super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
I create a common init for the designated and required. For convenience inits I delegate to init(frame:) with frame of zero.
Having zero frame is not a problem because typically the view is inside a ViewController's view; your custom view will get a good, safe chance to layout its subviews when its superview calls layoutSubviews() or updateConstraints(). These two functions are called by the system recursively throughout the view hierarchy. You can use either updateContstraints() or layoutSubviews(). updateContstraints() is called first, then layoutSubviews(). In updateConstraints() make sure to call super last. In layoutSubviews(), call super first.
Here's what I do:
#IBDesignable
class MyView: UIView {
convenience init(args: Whatever) {
self.init(frame: CGRect.zero)
//assign custom vars
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
commonInit()
}
private func commonInit() {
//custom initialization
}
override func updateConstraints() {
//set subview constraints here
super.updateConstraints()
}
override func layoutSubviews() {
super.layoutSubviews()
//manually set subview frames here
}
}
Swift 5 Solution
You can try out this implementation for running Swift 5 on XCode 11
class CustomView: UIView {
var customParam: customType
var container = UIView()
required init(customParamArg: customType) {
self.customParam = customParamArg
super.init(frame: .zero)
// Setting up the view can be done here
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupView() {
// Can do the setup of the view, including adding subviews
setupConstraints()
}
func setupConstraints() {
// setup custom constraints as you wish
}
}
Here is how I do it on iOS 9 in Swift -
import UIKit
class CustomView : UIView {
init() {
super.init(frame: UIScreen.mainScreen().bounds);
//for debug validation
self.backgroundColor = UIColor.blueColor();
print("My Custom Init");
return;
}
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented"); }
}
Here is a full project with example:
UIView Example Project (with SubView example)
Here is how I do a Subview on iOS in Swift -
class CustomSubview : UIView {
init() {
super.init(frame: UIScreen.mainScreen().bounds);
let windowHeight : CGFloat = 150;
let windowWidth : CGFloat = 360;
self.backgroundColor = UIColor.whiteColor();
self.frame = CGRectMake(0, 0, windowWidth, windowHeight);
self.center = CGPoint(x: UIScreen.mainScreen().bounds.width/2, y: 375);
//for debug validation
self.backgroundColor = UIColor.grayColor();
print("My Custom Init");
return;
}
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented"); }
}

Resources