Introducing Post Init

Note: I'm not sure if this issue affects all swift developers. This issue occurs for me very often when creating subclasses of UIKit components.

I'm missing the possibility to execute code directly after init was called. A lot of my custom UIKit look like this:

class CustomCell: UITableViewCell {
    public convenience init() {
        self.init(frame: .zero)
    }

    public override init(frame: CGRect) {
        super.init(frame: frame)
        setUpUI()
    }

    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setUpUI()
    }

    private func setUpUI() {
        // some post init setup code
        // Adding layout constraints could be done here,
        // since it requires the members to be initialized
        // and we want to avoid redundant code
    }
}

If there was a post init block there would be no need to to override the inits at all. In cases where some code is needed inside the init it would still provide a pattern to do so. So it would be consistent.

With post initialization bocks the code above would look like this:

class CustomCell: UITableViewCell {
    postInit { // similar to deinit
        // some post init setup code
        // Adding layout constraints could be done here,
        // since it requires the members to be initialized
        // and we want to avoid redundant code
    }
}

Since convenience inits always call another init they do not need to call the post init code. Designated inits will always call the post init code. The compiler could just copy the post init code to every every designated init or put a function call there.

In the Java enterprise environment the usage of PostConstruct annotations is common. Probably there're more use cases for server side swift. What do you think? Is this to specific or has to few use cases to add it to swift?

6 Likes

-1
Imagine debugging headaches this would cause.

class Foo: UIView {
  init(backgroundColor: UIColor) {
    super.init()
   self.backgroundColor = backgroundColor
}

class Bar: Foo {
  postInit {
    self.backgroundColor = .blue
  }
}

Then someone else tries to call Bar(backgroundColor: .red) and gets a view with a wrong color. There is a bug, so he tries to debug it by looking up implementation of init(backgroundColor:), but it looks fine. He sets up a breakpoint in the init, prints the background color with the debugger and it still looks fine. He runs his program, and it is broken!

3 Likes

Imho the root of the problem is that there are several independent inits... I guess this is part of the Objective-C heritage, and if you can avoid it, I always do.

5 Likes

This can be easily achieved otherwise:

class MyView: UIView {
	
	private lazy var _innerInit: Void = {
		self.backgroundColor = .red
	}()
	
	override func willMove(toSuperview newSuperview: UIView?) {
		super.willMove(toSuperview: newSuperview)
		
		_innerInit
	}
	
}

The lazy var will guarantee that the _innerInit is executed just once.

That might be true, but the issue there is that the implementation of Bar is wrong. So I don't know if that really applies since you have done the implementation by yourself. So I guess you would have a look at the class you are using. You can already do much worse stuff with the willSet and didSet, if you want to. You could compare this behavior to UIViewControllers in UIKit and I think you would definitely have a look at the viewWillAppear etc of the viewControllers. Since you know the lifecycle.

It would be straight forward:
UIView.init() -> UIView.postInit -> Foo.init(backgroundColor:) -> Foo.postInit -> Bar.init(backgroundColor) -> Bar.postInit

The language would guarantee a specific execution, so it is probably easier to debug. However I understand the argument that knowledge is required to deal with this kind of bugs.

This guarantees that it will be executed just once. But not when. Or if it will be executed at all.

1 Like

This is why it gets called in willMove(toSuperview:) which is always called before the view is added or removed from the view hierarchy. This is often what one needs...

Bugs happen. To prevent bugs like these you would have to:

  1. Every time you write postInit you check every init of every superclass
  2. Every time you add/modify init in Foo you check every subclass of Foo in your project.

You can already do much worse stuff with the willSet and didSet, if you want to

Not really. If I see that something weird is happening in the line spam.eggs = 123 I can cmd+click to the definition of var eggs and clearly see that there is didSet in there

you would definitely have a look at the viewWillAppear etc

We shouldn't try to make debugging init worse, just because debugging UIKit lifecycle is just as bad.

This is just specific to UIViewSubclasses. I probably should have chosen a non UIKit specific example.

I definitely agree. Bugs happen all the time. But keep in mind that cmd+click is an IDE feature. The same could be implemented for postInits. cmd+click could show all classes inits and postInits in the list.

I get your point and I agree to some degree, but this is about swift and not IDEs.

I might've as well said "go to the definition of var eggs" :)
My point is that I like behavior of X is in the definition of X, and there is no spooky action at a distance. I think the only example of spooky action at a distance right now is defer, which is written pretty close it's execution.

@edit
and deinit, but that one is necessary

1 Like

-1
This is a UIKit problem, and we shouldn't be tailoring Swift to address legacy design decisions that were implemented with another programming language in mind.

7 Likes

I think this is useful even on a pure Swift level to be able to leverage the synthesized initializers when customizations needed in the init. For example:

struct SomeType {
  let abc: String
  let def: String
}

let object = SomeType(abc: "123", def: "456")

If I wanted to add some common init or post init work, I have to manually implement the initializer:

struct SomeType {
  let abc: String
  let def: String

  init(abc: String, def: String) {
    self.abc = abc
    self.def = def

    // some extra work here
  }
}

It would be great if I could do the following to continue leveraging the synthesized initializer:

struct SomeType {
  let abc: String
  let def: String

  postInit {
    // some extra work here
  }
}

This becomes even more useful in SwiftUI because I'd have to manually implement the @Binding to the weird self._xyz = xyz and other property wrappers which get messy when I don't use the synthesized initializers.

3 Likes

This idea of postInit sounds like it could potentially be useful in any sort of Codable evolution to deal with deserializing reference types and lashing up relationships between them.

8 Likes

I think the struct example is a place where I can really see using this in pure swift. Also, any class (and Appkit and UIKit are great examples, but hardly the only place) where you may be overriding several inits and this could be useful.

I would love to have a simple way to do the following:

struct Foo {

   let bar : String 
   let qux = bar + "!"
//Cannot use instance member 'bar' within property initializer;
// property initializers run before 'self' is available

}

Maybe:

struct Foo {

   let bar : String 
   @lateInit let qux = bar + "!" //var also possible

}

I have some good news, it's already possible in the current version of swift! Check out the "Lazy Stored Properties" section Properties — The Swift Programming Language (Swift 5.4)

struct Foo {
   let bar : String
   lazy var qux = bar + "!"
}

yeah, unfortunately, this makes the getter mutating - which is not what I want in most cases. For classes, it works ok. Also, there's a semantic difference. lazy means that the property is computed when used. What I would like to have is a property that is eagerly initialized right after the properties it depends on.

4 Likes

What, if any, objections do you have to

struct Foo {
   let bar : String
   let qux: String

  init(bar: String) {
    self.bar = bar
    self.qux =  = bar + "!"
  }
}

The thing I like about (internal) structs is precisely that you don't need to spell out initializers in many cases. A "late init" feature that runs a topological sort algorithm over stored properties according to a dependency graph would make this way nicer.

The most obvious usecase is when the dependent (possibly mutable) property has a type that is easy to infer when writing how it is produced from the other properties, but that is hard to write down. For example, if SwiftUI's body had to be mutable for some reason, you couldn't write some View, but you would have to give an explicit type before initializing it. Or consider this:

protocol Function {
   associatedtype Input
   associatedtype Output
   func apply(to input: Input) -> Output
}

struct ComposedFunction<F1 : Function, F2 : Function> : Function
where F1.Output == F2.Input{...}

extension Function {
   func compose<Other : Function>(with other: Other) 
     -> ComposedFunction<Self, Other>{
      ...
   }
}

protocol FunctionWrapper : Function
 where Input == Body.Input, Output == Body.Output {
   associatedtype Body : Function
   var body : Body{get}
}

//...obvious default implementation of apply...

struct MyFunction : Function {

   let parameter : Double
   var body : ????? {
      F1().compose(with: F2(parameter)).compose(with: F3())
   }

}

Of course, you can write some Function for ?????, but as soon as you try to compose anything with MyFunction, you're in trouble (if the compiler even accepts some Function). If I could just write

let body = //crazy long compositional term dependent on parameter

There wouldn't be a problem.

Terms of Service

Privacy Policy

Cookie Policy