Can a UIView subclass create other custom view instances? - ios

I have a UIView subclass called LifeCounter, of which I want to create one for each player. I was able to add two views to Main.storyboard, set their class to LifeCounter through the Attributes panel and create multiple instances that way, connect to the view controller, change properties, etc.
What I was thinking about now was creating a larger view, GameHeader, that will hold the LifeCounters and some other supplementary information such as time, game reset button, etc. GameHeader is a UIView subclass, but I can't get it to draw my LifeCounter views in the simulator and I have no idea why.
GameHeader is currently a view dragged into the storyboard, and given it's class with the Attributes panel.
GameHeader.swift
import UIKit
class GameHeader: UIView {
// MARK: Properties
// The Frame
let topFrame = CGRect(x: 0, y: 0, width: 320, height: 200)
// MARK: Initlization
required init() {
super.init(frame: topFrame)
// Adds the player one counter
let playerOneCounter = LifeCounter()
addSubview(playerOneCounter)
}
required init(coder aDecoder: NSCoder) {
// Calls the super class (UIView) initializer
super.init(coder: aDecoder)
}
}
LifeCounter.swift
import UIKit
class LifeCounter: UIView {
// MARK: Propterties
// Starting life total
var lifeTotal = 20 {
didSet {
// Updates the layout whenever the lifeTotal is updated
setNeedsLayout()
}
}
// Creates the UI Labels
// All created views need a defined frame for where they sit
// Is this neccesary outside of the init? Is there a better way?
var counter = UILabel(frame: CGRect(x: 0, y: 20, width: 100, height: 90))
var playerName = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 40))
var winner = ""
// MARK: Initlization
// First init. LifeCounter takes a frame parameter, the adds the labels etc.
init() {
super.init(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
self.addLifeCounter()
}
required init(coder aDecoder: NSCoder) {
// Calls the super class (UIView) initializer
super.init(coder: aDecoder)
}
func addLifeCounter() {
print("addLifeCounter is running")
// Styles life counter label
counter.textColor = UIColor.blackColor()
counter.textAlignment = .Center
counter.font = UIFont.boldSystemFontOfSize(72)
counter.text = String(lifeTotal)
// Styles playerName label
playerName.text = "Player Name"
playerName.textAlignment = .Center
// Button
let minusButton = UIButton(frame: CGRect(x: 5, y: 110, width: 40, height: 40))
let plusButton = UIButton(frame: CGRect(x: 55, y: 110, width: 40, height: 40))
minusButton.backgroundColor = UIColor.redColor()
plusButton.backgroundColor = UIColor.blueColor()
// Button action
minusButton.addTarget(self, action: "minusLife:", forControlEvents: .TouchDown)
plusButton.addTarget(self, action: "plusLife:", forControlEvents: .TouchDown)
addSubview(playerName)
addSubview(counter)
addSubview(minusButton)
addSubview(plusButton)
}
// MARK: Button actions
func minusLife(minusButton: UIButton) {
lifeTotal -= 1
counter.text = String(lifeTotal)
}
func plusLife(plusButton: UIButton) {
lifeTotal += 1
counter.text = String(lifeTotal)
}
}

init(coder:) is the initializer that is called when a view is created from a storyboard or nib. If you move the code that creates and adds LifeCounter instances there, it should work.
A good strategy to make it more reusable is to create a setup method that is called from both initializers so that it will run whether it comes from a storyboard/nib or it is instantiated programmatically.
class GameHeader: UIView {
let topFrame = CGRect(x: 0, y: 0, width: 320, height: 200)
required init() {
super.init(frame: topFrame)
self.setupSubviews()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupSubviews()
}
func setupSubviews() {
let playerOneCounter = LifeCounter()
addSubview(playerOneCounter)
}
}

I discovered the error was in putting the LifeController drawing code inside the init(frame:) block instead of with the Coder block. When a view is added through Interface Builder, it uses init(coder:) instead of init(frame:)

Related

UITapGestureRecognizer not working with custom view class

UITapGestureRecognizer works perfectly fine in code 1. tapAction is called as expected.
However it's not working in code 2. Can somebody please tell me what's wrong in code 2?
(This and this are quite similar questions, but still couldn't figure it out) 🧐
Code 1:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let myView : UIView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
myView.backgroundColor = .red
myView.addGestureRecognizer( UITapGestureRecognizer(target:self,action:#selector(self.tapAction)) )
self.view.addSubview(myView)
}
#objc func tapAction() {
print("tapped")
}
}
Code 2:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let myView = MyView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
self.view.addSubview(myView)
}
}
class MyView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
initView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func initView(){
let myView : UIView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
myView.backgroundColor = .red
myView.addGestureRecognizer(UITapGestureRecognizer(target:self,action:#selector(self.doSomethingOnTap)))
addSubview(myView)
}
#objc func doSomethingOnTap() {
print("tapped")
}
}
You're creating a subview that in this particular case goes out of the parent bounds because the view main view has 100 height and 100 width and the subview is placed at x: 100 and y: 100, resulting to be positioned at the exact end of the parent.
The subview you create in initView should have (x: 0, y: 0) origin.

Creating custom UIView, or child VCs

So, i have the following case. I got a custom tab layout, ie a horizontal UIScollview with paging. In each page there are some controls and a UITablevie, "bundles" in a custom UIView.
I create a ContainerVC, which has the scrollview, and then I initialize the two UIViews and add them as subviews in ContainerVC. To do so, i have created a custom UIView class and have have set this UIView as the owner of the .xib file. I also add the .xib class as my custom UIView.
Although this works, at least UI wise, i have some functionality problems, like the following.
In the init method of each UIView I initialize the view with
Bundle.main.loadNibNamed(kCONTENT_XIB_NAME, owner: self, options: nil)
and then set the UITableViewDelegate and UITableViewDateSource to self.
However, when the data that feed the table arrives in the view, and i reload the tableview, nothing happens (the reload method is run in the main Thread), until i scroll the UITableview.
Is this the correct course of action?
VC
func createUsageHistoryTabs() -> [Consumption] {
var tabs = [Consumption]()
let v1 = Consumption(pageIndex: 0, viewModel: viewModel)
let v2 = Consumption(pageIndex: 1, viewModel: viewModel)
tabs.append(v1)
tabs.append(v2)
return tabs
}
func setupScrollView() {
if(!hasBeenCreated) {
let tabs = createUsageHistoryTabs()
scrollview.frame = CGRect(x: 0, y: 0, width: container.frame.width, height: container.frame.height)
scrollview.contentSize = CGSize(width: view.frame.width * CGFloat(tabs.count), height: 1.0)
scrollview.isPagingEnabled = true
for i in 0 ..< tabs.count {
let slideView = tabs[i]
slideView.frame = CGRect(x: view.frame.width * CGFloat(i), y: scrollview.frame.origin.y, width: scrollview.frame.width, height: scrollview.frame.height)
scrollview.addSubview(slideView)
}
hasBeenCreated.toggle()
}
}
UIView:
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
convenience init(pageIndex: Int, viewModel: UsageHistoryViewModel) {
self.init(frame: CGRect.zero)
self.pageIndex = pageIndex
self.viewModel = viewModel
commonInit()
}
func commonInit() {
Bundle.main.loadNibNamed(kCONTENT_XIB_NAME, owner: self, options: nil)
contentView.fixInView(self)
setupView()
footer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleFooterTap)))
viewModel?.cdrDelegate = self
tableView.register(EmptyCell.self, forCellReuseIdentifier: "EmptyCell")
tableView.register(UINib(nibName: "TVCell", bundle: nil), forCellReuseIdentifier: "TVCell")
tableView.allowsSelection = false
tableView.delegate = self
tableView.dataSource = self
}
In this func:
func setupScrollView() {
if(!hasBeenCreated) {
// this line creates a LOCAL array
let tabs = createUsageHistoryTabs()
scrollview.frame = CGRect(x: 0, y: 0, width: container.frame.width, height: container.frame.height)
scrollview.contentSize = CGSize(width: view.frame.width * CGFloat(tabs.count), height: 1.0)
scrollview.isPagingEnabled = true
for i in 0 ..< tabs.count {
let slideView = tabs[i]
slideView.frame = CGRect(x: view.frame.width * CGFloat(i), y: scrollview.frame.origin.y, width: scrollview.frame.width, height: scrollview.frame.height)
scrollview.addSubview(slideView)
}
hasBeenCreated.toggle()
}
}
You are creating a local array of Consumption objects. From each object, you add its view to your scrollView. As soon as that func exits, tabs no longer exists... it goes "out-of-scope".
You want to create a class-level var:
var tabsArray: [Consumption]?
and then modify that var instead of creating a local instance:
func setupScrollView() {
if(!hasBeenCreated) {
// this line creates a LOCAL array
tabsArray = createUsageHistoryTabs()
...
Your custom classes should then be available until you delete them (or tabsArray goes out-of-scope).
You may need to make some other changes, but that should, at least, be a start.
EDIT after comment discussion...
If the tableviews are working, but not "updating" until the are scrolled - you said reload is being called on the main thread - you might try either of these options (or both... experiment):
tableView.reloadData()
tableView.setNeedsLayout()
tableView.layoutIfNeeded()
tableView.reloadData()
tableView.beginUpdates()
tableView.endUpdates()

When inheriting UIView and creating an instance for the new class, it does not create a view

I have created a new class inheriting UIView. When I create an instance of the new class, the view does not seem to be created.
This is my class.
class QuestionView: UIView {
var metrics : [String : CGFloat] = [:]
override init(frame : CGRect){
super.init(frame: frame)
self.backgroundColor = UIColor.blue
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here I have created an instance of this class to have the following values
x = 0, y = middle of the screen, width = as wide as the screen,
height = 400
class mainView : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var questionView = QuestionView(frame: CGRect(x: 0, y: self.view.frame.height/2, width: self.view.frame.width, height: 400))
self.view.addSubView(questionView)
}
}
I find no view being created.
Add subview(questionView) in mainView class in viewDidLoad method.
self.view.addSubview(questionView)
you can also test the view by setting the background color
questionView.backgroundColor = UIColor.red
You should add all views created programmatically in viewDidLayoutSubView like below
override func viewDidLayoutSubviews() {
var questionView = QuestionView(frame: CGRect(x: 0, y: self.view.frame.height/2, width: self.view.frame.width, height: 400))
self.view.addSubView(questionView)
}
Try following code to get the view in front, use bringSubviewToFront function.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let questionView = QuestionView(frame: CGRect(x: 0, y: self.view.frame.height/2, width: self.view.frame.width, height: 400))
self.view.addSubview(questionView)
self.view.bringSubviewToFront(questionView)
}

UIButton in scrollView position unexpected

I created a very simple custom class from UIView. On the top there is a UIScrollView. Inside the UIScrollView, I placed a couple of buttons. The buttons position is out of the scrollview.
Here is the code.
class TestView: UIView {
var tabView: UIScrollView = UIScrollView()
var buttons = [UIButton]()
var tabHeight: CGFloat = 40.0
let screenWidth: CGFloat = UIScreen.mainScreen().bounds.width
var titles = [String]()
var numberOfTitles = 0
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addSubview(tabView)
}
func createUI() {
tabView.frame = CGRect(x: 0, y: 0, width: screenWidth, height: tabHeight)
tabView.backgroundColor = UIColor.grayColor()
tabView.contentSize = CGSize(width: screenWidth, height: tabHeight)
numberOfTitles = titles.count
for var i = 0; i < numberOfTitles; ++i {
var button = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
button.frame = CGRect(x: 50.0*CGFloat(i), y: 0, width: 50.0, height: tabHeight)
button.setTitle(titles[i], forState: UIControlState.Normal)
button.setTitleColor(UIColor.blackColor(), forState: UIControlState.Normal)
tabView.addSubview(button)
}
}
}
Here is how it looks when I debug view Hierarchy.
But if I just change the UIScrollView to UIView, as below,
var tabView: UIView = UIView()
Then the button locations are correct, as below.
I could not figure out why. Please help. Thanks!
This is because UIScrollView by itself only is able to scroll a container view. What you need to do is initialize a UIScrollView like you did originally, but instead of adding all the components to the scrollview, create a new UIView called conatinerView, change all the tabView to containerView, then change the required init to:
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addSubview(tabView)
tabView.addSubview(containerView)
}
But make sure the frame size for containerView is correct and so is the origin point.

Strange behaviour - instantiating a UITextField in willMoveToSuperview as opposed to didMoveToSuperview

I'm working with XCode 6.3/iOS/Swift 1.2. I've just stumbled upon some strange behaviour that I don't understand (coming from a strong OO background).
Put simply, if I subclass a UIView and use it to instantiate some UITextFields; in the constructor, overriding didMoveToSuperview, on dynamically (ie, with a click event)... the UITextFields work as expected. However, when creating the UITextField object during willMoveToSuperview, it seems fine... but it doesn't respond to touch events.
This is demonstrated in the code below (a UIViewController and the subclassed UIView). I've added a gesture recognizer to the entire view containing the UITextFields. Clicking any of them other than the one creating during willMoveToSuperview, will move the focus to that textfield, and will ignore the touch event (as expected). However, clicking on the one added during willMoveToSuperview fires a touch event. Event without the touch event... this textfield remains unresponsive. Can anyone explain this behaviour?
import UIKit
class RootViewController:UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
self.view.frame = CGRectMake(0,0,500,500)
}
override func viewDidAppear(animated: Bool)
{
var uiview = ExtendedUIView()
view.addSubview(uiview)
}
}
class ExtendedUIView:UIView
{
internal var hasMovedToSuperView:Bool = false
override init (frame : CGRect)
{
super.init(frame : frame)
}
convenience init()
{
self.init(frame:CGRectMake(0,0,500,500))
var tf: UITextField = UITextField(frame: CGRect(x: 0, y: 50, width: 300, height: 40))
tf.backgroundColor = UIColor.greenColor()
tf.text = "init // ok"
addSubview(tf)
}
required init(coder aDecoder: NSCoder)
{
fatalError("This class does not support NSCoding")
}
override func willMoveToSuperview(newSuperview: UIView?)
{
if (!hasMovedToSuperView) {
var tf: UITextField = UITextField(frame: CGRect(x: 0, y: 100, width: 300, height: 40))
tf.backgroundColor = UIColor.redColor()
tf.text = "willMoveToSuperview // not ok"
addSubview(tf)
}
}
override func didMoveToSuperview()
{
if (!hasMovedToSuperView) {
var tf: UITextField = UITextField(frame: CGRect(x: 0, y: 150, width: 300, height: 40))
tf.backgroundColor = UIColor.greenColor()
tf.text = "didMoveToSuperview // ok"
addSubview(tf)
hasMovedToSuperView = true
addGestureRecognizer(UITapGestureRecognizer(target:self, action:Selector("createTFDynamically:")))
}
}
func createTFDynamically(value:AnyObject)
{
var tf: UITextField = UITextField(frame: CGRect(x: 0, y: 200, width: 300, height: 40))
tf.backgroundColor = UIColor.greenColor()
tf.text = "createTFDynamically // ok"
addSubview(tf)
}
}

Resources