26

I have a UIViewController called LoginViewController. I want to build the view of that LoginViewController fully programmatically in a custom UIView class called LoginView instead of building all the elements within my LoginViewController. This way I'm preventing "View" code in a Controller class (MVC).

In the code below I'm setting the view of my LoginViewController to my LoginView which for simplicity only contains 2 UILabels

class LoginViewController: UIViewController {

override func loadView() {
    super.loadView()

    self.view = LoginView(frame: CGRect.zero)
}

The LoginView class initialises both labels and should set some constraints.

class LoginView: UIView {

var usernameLabel: UILabel!
var passwordLabel: UILabel!


override init (frame : CGRect) {
    super.init(frame : frame)

    setupLabels()
}

convenience init () {
    self.init(frame:CGRect.zero)
}

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

private func setupLabels(){
    //Init labels and set a simple text
    self.usernameLabel = UILabel()
    self.usernameLabel.text = "Username"
    self.passwordLabel = UILabel()
    self.passwordLabel.text = "Password"

    //Set constraints which aren't possible since there is no contentView, perhaps using the frame?
    }
}

This doesn't work since the view's bounds are 0. However I couldn't find any resource that gives insight in whether this is possible, so I tried my approach which didn't work.

How you set the view of a UIViewController to a custom UIView which is made programmatically? Or is the above snippet recommended?

This is the working solution based on Jadar's answer:

class LoginViewController: UIViewController {

    override func loadView() {
        view = LoginView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()        
    //  Do any additional setup after loading the view.

    }
}

class LoginView: UIView {

var usernameLabel: UILabel!
var passwordLabel: UILabel!

override init(frame: CGRect) {
    super.init(frame: frame)

    self.usernameLabel = UILabel()
    self.usernameLabel.text = "Username"
    self.passwordLabel = UILabel()
    self.passwordLabel.text = "Password"

    addSubview(usernameLabel)
    addSubview(passwordLabel)

    if let superview = usernameLabel.superview{
        //Setting AutoLayout using SnapKit framework
        usernameLabel.snp.makeConstraints { (make) in
            make.center.equalTo(superview)
        }
    }
}

Result:

Result

Hapeki
  • 2,153
  • 1
  • 20
  • 36
  • 1
    The code in your updated question isn't correct for the view controller. Do not set self.view in viewDidLoad. Do it in loadView like you did originally. – rmaddy Dec 31 '16 at 02:53
  • Thank you for your feedback! @rmaddy – Hapeki Dec 31 '16 at 17:10

4 Answers4

28

It looks there are really two questions here. One, what is the best way to programmatically set up a ViewController. The other, how to set up a View programmatically.

First, The best way to have a ViewController programmatically use a different UIView subclass is to initialize and assign it in the loadView method. Per Apple's docs:

You can override this method in order to create your views manually. If you choose to do so, assign the root view of your view hierarchy to the view property. The views you create should be unique instances and should not be shared with any other view controller object. Your custom implementation of this method should not call super.

This would look something like this:

class LoginViewController: UIViewController {    
    override func loadView() {
        // Do not call super!
        view = LoginView()
    }
}

This way you shouldn't have to deal with sizing it, as the View Controller itself should take care of it (as it does with it's own UIView).

Remember, do not call super.loadView() or the controller will be confused. Also, the first time I tried this I got a black screen because I forgot to call window.makeKeyAndVisible() in my App Delegate. In this case the view was never even added to the window hierarchy. You can always use the view introspecter button in Xcode to see what's going on.

Second, you will need to call self.addSubview(_:) in your UIView subclass in order to have them appear. Once you add them as subviews, you can add constraints with NSLayoutConstraint.

private func setupLabels(){
    // Initialize labels and set their text
    usernameLabel = UILabel()
    usernameLabel.text = "Username"
    usernameLabel.translatesAutoresizingMaskIntoConstraints = false // Necessary because this view wasn't instantiated by IB
    addSubview(usernameLabel)

    passwordLabel = UILabel()
    passwordLabel.text = "Password"
    passwordLabel.translatesAutoresizingMaskIntoConstraints = false // Necessary because this view wasn't instantiated by IB
    addSubview(passwordLabel)

    NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|-10-[view]", options: [], metrics: nil, views: ["view":usernameLabel]))
    NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|-20-[view]", options: [], metrics: nil, views: ["view":passwordLabel]))

    NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-10-[view]", options: [], metrics: nil, views: ["view":usernameLabel]))
    NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-20-[view]", options: [], metrics: nil, views: ["view":passwordLabel]))
}

For more info on the visual format language used to create the constraints, see the VFL Guide

Jadar
  • 1,643
  • 1
  • 13
  • 25
  • This doesn't answer the question at all. The OP was originally doing this. That wasn't the problem. The problem is with the implementation of the custom view, not with how the view is created in the view controller. – rmaddy Dec 31 '16 at 03:25
  • 1
    Actually, I think we both misunderstood the OP's question. It sounds like the real problem is that his labels aren't showing up, which he attributes to their frames being zero'd. But that's because 1, he never adds them as subviews and 2, he never sizes them or positions them. I've updated my answer to reflect both the controller and layout questions. – Jadar Dec 31 '16 at 14:15
  • I always knew that was the problem. My answer addressed the sizing issue from the beginning. – rmaddy Dec 31 '16 at 14:43
  • This is exactly what I've been searching for. Thanks for your detailed and clear explanation! I'll mark this as the accepted answer and I'll update my question. Thanks again! – Hapeki Dec 31 '16 at 17:00
3

Override the layoutSubviews method to update the frames of the subviews inside your custom view.

And never call super.loadView(). This is documented for the loadView method.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • Thank you for your reply. How would I update the frames using AutoLayout (SnapKit) in the layoutSubviews if my frame is CGRect.zero though? I can't find a way to make the passwordLabel center to its superView .. since there is none it seems? – Hapeki Dec 31 '16 at 01:09
  • If you want to use auto layout, then set the initial frame to a non-zero frame, and setup the auto layout when you create the subviews initiallly. Then you don't need to implement `layoutSubviews`. – rmaddy Dec 31 '16 at 01:12
  • This answer doesn't provide adequate information to help the OP fix his/her problem. – Jadar Dec 31 '16 at 14:20
2

You should load the custom view when LoginViewController's layout constraints are already loaded, try this:

  override func viewDidLoad() {
      super.viewDidLoad()
      let newView = LoginView(frame: view.bounds)
      view.addSubview(newView)
    }
DariusV
  • 2,645
  • 16
  • 21
  • Thank you for your reply. I edited my post with a potential solution based on your answer. If this solution is accepted by the community as a valid solution I'll mark this answer as accepted. I'm not certain if calling viewDidLoad to setup the view is a good thing since Apple states: Implement loadView to create a view hierarchy programmatically, without using a nib. Implement viewDidLoad to do additional setup after loading the view, typically from a nib. – Hapeki Dec 31 '16 at 01:49
  • 3
    This is one possible solution but the original solution of using loadView is the proper place to set a view controller's view to a custom view. – rmaddy Dec 31 '16 at 02:51
  • If you want to use loadView you need to initialize your custom view with a frame other than the current view controller 'cause this is not loaded yet, you can set `UIScreen.main.bounds` to LoginView's frame. I you need a example just let me know. – DariusV Dec 31 '16 at 16:18
  • Try this http://stackoverflow.com/questions/7788928/loadview-functions-in-uiview-ios – DariusV Dec 31 '16 at 16:25
2

In your Viewcontroller's loadView method do this:

class LoginViewController: UIViewController {
    override func loadView() {
        super.view = LoginView()
    }
}

In your UIView's custom class do this:

class LoginView: UIView {
    convenience init() {
        self.init(frame: UIScreen.main.bounds)
        setupLabels()
    }
}

Now your UIView has a frame , and you can setup all your views through code by providing them frames.

Ashish
  • 1,899
  • 20
  • 22