How to Implement Two Inits with Same Content Without Code Duplication in Swift

How to implement two inits with same content without code duplication in Swift?

I just had the same problem.

As GoZoner said, marking your variables as optional will work. It's not a very elegant way because you then have to unwrap the value each time you want to access it.

I will file an enhancement request with Apple, maybe we could get something like a "beforeInit" method that is called before every init where we can assign the variables so we don't have to use optional vars.

Until then, I will just put all assignments into a commonInit method which is called from the dedicated initialisers. E.g.:

class GradientView: UIView {
var gradientLayer: CAGradientLayer? // marked as optional, so it does not have to be assigned before super.init

func commonInit() {
gradientLayer = CAGradientLayer()
gradientLayer!.frame = self.bounds
// more setup
}

init(coder aDecoder: NSCoder!) {
super.init(coder: aDecoder)
commonInit()
}

init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}

override func layoutSubviews() {
super.layoutSubviews()
gradientLayer!.frame = self.bounds // unwrap explicitly because the var is marked optional
}
}

Thanks to David I had a look at the book again and I found something which might be helpful for our deduplication efforts without having to use the optional variable hack. One can use a closure to initialize a variable.

Setting a Default Property Value with a Closure or Function

If a stored property’s default value requires some customization or setup, you can use a closure or global function to provide a customized default value for that property. Whenever a new instance of the type that the property belongs to is initialized, the closure or function is called, and its return value is assigned as the property’s default value. These kinds of closures or functions typically create a temporary value of the same type as the property, tailor that value to represent the desired initial state, and then return that temporary value to be used as the property’s default value.

Here’s a skeleton outline of how a closure can be used to provide a default property value:

class SomeClass {
let someProperty: SomeType = {
// create a default value for someProperty inside this closure
// someValue must be of the same type as SomeType
return someValue
}()
}

Note that the closure’s end curly brace is followed by an empty pair of parentheses. This tells Swift to execute the closure immediately. If you omit these parentheses, you are trying to assign the closure itself to the property, and not the return value of the closure.

NOTE

If you use a closure to initialize a property, remember that the rest of the instance has not yet been initialized at the point that the closure is executed. This means that you cannot access any other property values from within your closure, even if those properties have default values. You also cannot use the implicit self property, or call any of the instance’s methods.

Excerpt From: Apple Inc. “The Swift Programming Language.” iBooks. https://itun.es/de/jEUH0.l

This is the way I will use from now on, because it does not circumvent the useful feature of not allowing nil on variables. For my example it'll look like this:

class GradientView: UIView {
var gradientLayer: CAGradientLayer = {
return CAGradientLayer()
}()

func commonInit() {
gradientLayer.frame = self.bounds
/* more setup */
}

init(coder aDecoder: NSCoder!) {
super.init(coder: aDecoder)
commonInit()
}

init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
}

Create a cleaner way to set up a struct with multiple inits?

I don't know what cleaner means in your understanding. There are a lot of ways to reduce inits in your code. You can give them a default values e.g., or create some other struct named DestinationParams that will include all params you need.

struct Destination {
private (set) var name: String
private (set) var waypoint: JMapWaypoint?

init(destination: JMapDestination? = nil) {
self.init(name: destination?.name ?? "Shared Location",
waypoint: destination?.waypoints?.first)
}

init(name: String, point: JMapPoint? = JMapPoint(), mapController: JMapController) {
let waypoint = mapController.getNearestWaypoint(with: point)
self.init(name: name,
waypoint: waypoint)
}

private init(name: String, waypoint: JMapWaypoint?) {
self.waypoint = waypoint
self.name = name
}
}

or, you can modify it by moving all computation outside of init like this:

...
let pointToJMapPoint = JMapPoint.init(x: Float(point.x),
y: Float(point.y),
z: Float(mapController.userLocation.mapId))
let point = mapController.getNearestWaypoint(with: pointToJMapPoint ?? JMapPoint())
let x = Destination(name: "Some name", waypoint: point)
...
struct Destination {
private (set) var name: String
private (set) var waypoint: JMapWaypoint?

init(destination: JMapDestination? = nil) {
self.init(name: destination?.name ?? "Shared Location",
waypoint: destination?.waypoints?.first)
}

init(name: String, waypoint: JMapWaypoint?) {
self.waypoint = waypoint
self.name = name
}
}

It is all up to you and depends on what you want.

How to avoid duplicate code objective-c

Yeah this is a problem with the lack of multiple inheritance in Objective-c. I had the same problem when needing certain methods on a subclass of UIView and UIScrollView separately here: Subclassing UIView vs UIScrollView. There are 3 possible solutions I know of:

  1. If you don't need to store any kind of instance variable, simply declare a category on UIScrollView and make sure to import that category into the two subclasses. This is the easiest solution, but least likely to work since you probably need to store state information if you're subclassing anyway.
  2. Only create a subclass of UITableView and simply don't use it as a UITableView when you don't want a UITableView. You can technically just use a UITableView as a UIScrollView without invoking any of the tableView's methods. Of course, you're going to end up carrying around the 'weight' of a tableView (all of it's instance variables) but there no reason you have to use a UITableView as a UITableView and not just a UIScrollView.
  3. Delegate as much of your code to a separate object to minimize code duplication. In each separate subclass carry an instance variable that is the method delegate and forward method calls to that delegate. Now here's where it gets fun. You can use protocols to declare the delegate methods in your subclass and override a special NSObject method: - (id) forwardingTargetForSelector:(SEL)aSelector to make sure those method calls get sent to the delegate. You use a category on the subclass that conforms to the protocol declared in the delegate class. This will expose all the methods of the delegate class in the subclass without requiring you to actually implement those methods in the subclass. When the runtime can't find the declared method in the subclass, it will call - (id) forwardingTargetForSelector:(SEL)aSelector, which you can use to return your delegate/forwarded class. This will prevent you from needing forward each individual method. Depending on what those method calls do, this may take a little more 'wiring', but it'll save you a lot of code writing in the end. It essentially 'mimics' multiple inheritance in objective-c using protocols. See my question/answer here for more details: https://stackoverflow.com/a/9419587/1147934.

Of the three, the last option tends to work the best for me. It takes a little work to get your head around but will significantly reduce code duplication. I also use it when I want to subclass without subclassing. The biggest requirement, though, is any class that you want to do this with will have to move it's method declarations out of it's interface into a separate protocol. But it's really not a big deal and the benefits of getting 'multiple inheritance like behavior' is great.

Also, there are times you may need the forwarded class to access instance variables in the forwarding class (the subclass). You can achieve this by using a delegate pattern whereby the forwarded class maintains a weak reference to the forwarding class in order to access those instance variables. For example, in your case, if you're trying to delegate methods that operate on a UIScrollView, those methods may need to be able to access that view. If those methods are stuck in a delegate class, they won't have direct access to the view's variables unless you give it to them. As usual with any delegate pattern, be very careful you don't create a retain cycle.

How can I initialize a let variable using a shared init method?

I found this answer and liked it's approach. It fixed the problem, and is much cleaner IMHO. By implementing init(coder aDecoder: NSCoder) normally we would be keeping up the appearance that this view can be initialized from a nib/storyboard when it can't.

class SuperCoolView : UIView {

let imageView: UIImageView

required init(coder aDecoder: NSCoder) {
fatalError("NSCoding not supported")
}

override init(frame: CGRect) {
self.imageView = UIImageView.new()
// Other stuff
super.init(frame: frame)
}

}



Related Topics



Leave a reply



Submit