Accessing Programatic View from the ViewController? - ios

Im building my views programatically, but need to refer to the views in my controller for setting properties. I cant see a way to achieve this?
I have a reference to the view as of course the controller inits it, but then i cant access properties for example trying self.view.textField as id thought i would.
How do I achieve this? And vice versa how do I do things like setting the views textField to use the controller as its delegate to handle its input?
First time ive not used storyboard where all this was simpler but messier, appreciate any clarification on these issues

It's easy.
Initialize your UI elements directly in UIViewController.
For example:
private let yourLabel: UILabel = {
let label = UILabel()
label.text = "Text"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
private func setupViews() {
view.addSubview(yourLabel)
// Setup constraints for yourLabel
}
Then access yourLabel anywhere in the controller code. It works for all the UI components.
NOTE!
You can setup all the views not in the controller, but in a separate view. And then add only one view (with other subviews inside) to the controller. You will have access to all subviews using this one view. But the way how you create UI components in the separate view will be the same:
let yourSubView: UILabel = { // any UI component
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
Example of a custom separate UIView:
class CustomView: UIView { // Separate view. Doesn't make the controller massive
let yourSubView: UILabel = { // any UI component
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func setupViews() {
addSubview(yourSubView)
// Setup yourSubView constraints
}
}
Then you can add CustomView to UIViewController and access all the subviews of CustomView.

Related

Right way to make a list with images inside a tableview cell SWIFT

I currently have a UITableView in which each cell contains a list of users. I currently have it setup using a UITextView and line break after each name. I then set the cell height accordingly to how many people are in the group.This is working great but now I want to add profile pictures by each use. Here are two possible solutions I have come up with.
A tableview inside each table view cell
Using a method like this in order to add the image in as text and keep the current textview. I believe this would be the better/ more efficient option.
Which method of these two or other option would be the right way/ or most efficient way of going about doing this.
UPDATE : I am trying to implement a solution from below here is what I have tried but I am seeing nothing when running
ADDING STACK VIEW IN STORYBOARD
CREATING USER VIEW CLASS
import UIKit
#available(iOS 9.0, *)
class UserView: UIStackView {
var name: String?
var image: UIImage? // OR var imageName: String?
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
axis = .vertical
let imageView = UIImageView(image: image)
let nameLabel = UILabel()
nameLabel.text = name
addArrangedSubview(imageView)
addArrangedSubview(nameLabel)
}
}
CODE IN CELL FOR ROW AT INDEX PATH
cell.mainStackView.frame = CGRect(x: 0, y: 0, width: cell.cellColor.frame.width, height: cell.cellColor.frame.height)
for i in 0..<joinedArray.count {
let user = UserView()
user.name = joinedArray[i]
cell.mainStackView.addArrangedSubview(user)
}
I would prefer UIStackView inside a UITableViewCell and UIStackView as a subview for each user entry
Something like below mentioned
class UserView: UIStackView {
var name: String?
var image: UIImage? // OR var imageName: String?
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
axis = .vertical
let imageView = UIImageView(image: image)
let nameLabel = UILabel()
nameLabel.text = name
addArrangedSubview(imageView)
addArrangedSubview(nameLabel)
}
}
Instead of putting a UITableView inside a UITableViewCell, you could try to put a UICollectionView in a UITableViewCell. Technically it's the same but with the collection view you get more versatility in the way you can have your images displayed in different layouts (assuming that each UICollectionViewCell takes a UIImageView for each respective image). Ash Furrow has a really nice blog post on this as well as the respective GitHub rep.

Why can’t see the preview of my custom view in storyboard?

When the app run in Simulator it works but in Storyboard can’t see the preview, why?
Swift code:
Simulator and Storyboard:
Can’t see the custom view background color and when drag UIButton object into custom view doesn’t see the real position(x, y) and its background color.
In Android Studio when you add an object (custom view) in layout.xml file you can see the preview automatically, is it possible to do the same thing in Xcode?
TL;DR; Call in prepareForInterfaceBuilder
// Call The Custom Setup Here
override func prepareForInterfaceBuilder() {
setupView()
}
Calling in layoutSubviews also works, but is called multiples times in runtime, prepareForInterfaceBuilder is called only for Designables Changes, and only with this purpose.
Long Code:
#IBDesignable
class CustomView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
override func prepareForInterfaceBuilder() {
setupView()
}
func InitChildPosition() {
var i = 1
for _view in self.subviews {
if _view is UIButton {
_view.center.x = (_view.bounds.width / 2)
_view.center.y = (_view.bounds.height / 2)
}
if _view is UIButton && i == 2 {
_view.center.x = self.bounds.width - (_view.bounds.width / 2)
_view.center.y = self.bounds.height - (_view.bounds.height / 2)
_view.backgroundColor = UIColor.darkGray
}
i += 1
}
}
func setupView() {
self.backgroundColor = UIColor.black
InitChildPosition()
}
}
Call your CustomSetup() from init(frame:)
EDIT
Interface Builder uses init(frame:) to create and position views in storyboards.
You are modifying the subviews of CustomView in CustomSetup() which is called from the initializer. Your view does not have any button subviews in that case, because subviews can only be added after the view has been initialized.
You need to defer calling CustomSetup() to a later point, awakeFromNib() should work in your case.
Better yet, remove CustomSetup() completely and just style your UIButtons in the Interface Builder. You will get the same results without all the complexity.

Set UIViewController view property to custom UIView class without storyboard / nib

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:
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
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.
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)
}
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.

XCode: Is it possible to create reusable stack views?

Is it possible to create reusable stack views on the story board that can be used dynamically to be generated at a later time? Sort of a template/widget/component.
I am aware that I can do this with a class but if I am able to visually generate a set of components that can be re-used at a latter time I may be able to let our designers make changes to storyboards directly.
Yes -- you can do this with any UIView. There are many tutorials for this (e.g. http://onedayitwillmake.com/blog/2013/07/ios-creating-reusable-uiviews-with-storyboard/)
The basic idea is to drag one onto Storyboard or XIB, make a custom class for it, then implement the view's awakeFromNib to load it.
Yes.It is.
Create a empty xib and then add a stack view to it.
Then create a class which extends UIStackView.
class stackView: UIStackView {
var contentView : UIStackView!
override init(frame: CGRect) {
super.init(frame: frame)
xibSetup()
}
required init(coder: NSCoder) {
super.init(coder: coder)
xibSetup() }
func xibSetup() {
contentView = loadViewFromNib()
contentView.frame = bounds
contentView.autoresizingMask = [UIViewAutoresizing.flexibleWidth, UIViewAutoresizing.flexibleHeight]
addSubview(contentView)
}
func loadViewFromNib() -> UIStackView! {
let view: UIStackView? = Bundle.main.loadNibNamed("stackView", owner: nil, options: nil)?.first as! UIStackView?
return view
}
Create a viewController.Add a stackView to it.In StackView properties, goto 3rd bar which named as custom class, for class name give stackView class name

How to make reusable UIView template consisiting UILabels

I'm building a weather app as a beginner project. Say I wanted a custom view that consisted on many UILabels for temp, humidity, precipitation, etc. The idea is that this custom UIView would be used several times for every city the user has saved. If the user has 3 cities saved, the custom view would have 3 instances.
What is the best way to do this? I'm trying to subclass a UIView. Originally I was overriding drawRect(rect: CGRect) and defining my UILabels there. That just didn't feel right. And it wouldn't get alloc/inited until way later, after I was trying to update the label text in the completion handler on NSURLSession.
Or should I be overriding init() which makes me do this:
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
And I have no idea what that means. Then I'm forced to doing something like this when I try to init with frame on the root VC.
override init(frame: CGRect) { super.init(frame: frame) }
Can someone walk me through the best approach? I have something like below but I get a nil value right when I'm trying to add the UILabels to subview of the custom class.
class ViewTemplate: UIView {
var tempLabel: UILabel!
var humidityLabel: UILabel!
override init () {
tempLabel = UILabel()
tempLabel.frame = CGRectMake(halfScreenWidth - 130, 120, 260, 130)
tempLabel.textColor = UIColor.whiteColor()
tempLabel.backgroundColor = UIColor.clearColor()
// similar stuff for humidityLabel
super.init()
addSubview(tempLabel)
}
override init(frame: CGRect) { super.init(frame: frame) }
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If not quite sure where the nil is coming from. But most importantly, I'm looking for the best practice in doing this.
Thanks!
For creating the individual View:
Create new View file. Subclass of UIView. Be sure to also create the xib file when doing this.
Draw your items in storyboard (labels, etc.) and connect them to the swift file
Be certain that you connect them in storyboard by making the xib file a Custom Class of the swift file you just created.
Instantiate them inside the awakeFromNib() method. Be sure to set their default values for text if they are labels. Otherwise they will come up as nil when instantiated and your app will crash.
import UIKit
class MyView: UIView {
#IBOutlet weak var templabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
self.tempLabel.text = "95 degrees"
}
For loading into viewcontroller:
1a. Create a viewcontroller that implements UIScrollView.
1b. Place a UIScrollview in your storyboard and connect it to that viewcontroller
class MyWeatherViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet var scrollView: UIScrollView!
2a. In ViewDidLoad, create instances of the UIView you are looking to create. Populate them individually.
2b. In ViewDidLoad, set the scrollview width to the width of a single View. Set the height to the height of the single View * the number of Views you wish to display
var view1: MyView = MyView()
var view2: MyView = MyView()
let viewArray[MyView!] = [view1, view2]
for items in viewArray {
var frame = scrollView.bounds
//Set the origin of the views
frame.origin.x = 0.0
frame.origin.y = frame.size.height * items
//create a view out of the object provided, define it's frame, and add to scrollview
let viewToAdd = viewArray[items].view
newPageView.frame = frame
scrollView.addSubview(viewToAdd)
//Set up your labels to be displayed. You must do this AFTER you load the load into the scrollview
viewArray[items].tempLabel.text = "105 degrees"
}
Keep in mind that you will want to set up another data model to hold the information that is to be displayed. I would recommend creating an array that holds the information you wish to display, so you can do viewArray[items].tempLabel.text = tempArray[items] with all the labels you wish to set.
This should align them vertically with the ability to scroll and see all items.
This will also let dynamically decide how many views to add based on cities the user has saved. Just modify the logic to read however many cities the user has etc.
You're on the right track.
Declare the properties for the labels. It looks like you've already done that. Personally, I would declare those as constants with let instead of var because you should always have those labels.
Override the init(frame: CGRect) method. Again, it looks like you've already started this. The reason you should override init(frame: CGRect) instead if init() is because init() just calls init(frame: CGRectZero). If you only implement init() then some of your properties may not get initialized (although this is way less of an issue with Swift since you can declare properties as constants so the compiler throws an error). In your initializer you're going to want to init all your labels & set them up right before you call super.init(frame). After the super.init(frame) method is called you should add the labels as subviews of self.
Override layoutSubviews() if you are laying your UI out programmatically. This method is where you will do all the sizing and laying out of your subviews. I do everything this way so it's what I'm most familiar with. If you're using AutoLayout I believe it's best practice to apply those constraints in the class's initializer since they should only ever need to be set up once.
Optionally, you may also want to override sizeThatFits(). This will ensure your view is always the proper size when sizeToFit() is called on it.
This goes not just for UIView but any subclass of it.
You can use a UIViewController extension and initialize a UIView function that takes a UIView. You can use this anywhere in your project.
extension UIViewController {
func setView(_ view: UIView){
view.backgroundColor = UIColor()
view.layer.cornerRadius = 5.0
// customize your view
}
}
Then: Declare you view in your Controller and pass it when the function is called on viewDidLoad()
let myView = UIView()
setView(myView)

Resources