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

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

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.

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

Swift add Custom View to screen with UIApplication.shared.keyWindow?.addSubview

hope this is an easy one...
I am in a empty new project.
I have added a custom view called MyCustomView:
import UIKit
public class MyCustomView: UIView{
private var littleView: UIView!
open class func show() -> UIView{
let bigView = MyCustomView()
bigView.configureView()
UIApplication.shared.keyWindow?.addSubview(bigView)
return bigView
}
private func configureView(){
let screenSize = UIScreen.main.bounds.size
self.frame = CGRect(x: 0,
y: 0,
width: screenSize.width,
height: screenSize.height)
littleView = UIView(frame: CGRect(x: 10, y: 10, width: 100, height: 100))
littleView.backgroundColor = .black
addSubview(littleView)
}
}
In the ViewController doing this:
override func viewDidLoad() {
let test = MyFirstView.show()
}
I hoped this will present the view, but I still have to use self.view.addSubview(test) to see it....
I thought with UIApplication.shared.keyWindow?.addSubview(bigView) and adding a subView to it, it should present the View.
What am I missing?
Add the subview in viewDidAppear
override func viewDidAppear() {
let test = MyFirstView.show()
}

Darkened overlay while user is typing

When the user taps on a text field, I wanted to darken the rest of the screen (everything below the text box, above the keyboard) to make it clear what they should be doing. I believe it involved putting a transparent UI view down and adding a gesture recognizer to it, but I'm not quite sure how to do that.
I've got the following code for when the user arrives on the screen. Is this where I would add the new UI View?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
textField.becomeFirstResponder()
}
Thanks!
I created a subclass of UIView for two reasons:
you can add a gesture recogniser to it
you can add a delegate to it
Since the last subview added to a view is on top I first add the dark layer and then call bringSubviewToFront(textview) this will put the dark layer between the textview in question and everything else.
I created a protocol with one function. This function get's called by the gesture recogniser and returns the instance of DarkView to the delegate. The delegate (your ViewController) can then remove it from it's superview.
This you can do without a delegate function, but you also have to call resignFirstResponder() on the textfield.
Don't forget to set up the delegate of the DarkView in your ViewController.
Just a VC with some textfields.
class FirstViewController: UIViewController, DarkViewDelegate, UITextFieldDelegate {
var masterView : UIView!
override func viewDidLoad() {
super.viewDidLoad()
masterView = UIView(frame: self.view.frame)
masterView.backgroundColor = UIColor.whiteColor()
let textField1 = UITextField(frame: CGRect(x: 50, y: 20, width: 300, height: 20))
let textField2 = UITextField(frame: CGRect(x: 50, y: 60, width: 300, height: 20))
let textField3 = UITextField(frame: CGRect(x: 50, y: 100, width: 300, height: 20))
let textField4 = UITextField(frame: CGRect(x: 50, y: 140, width: 300, height: 20))
func styleTextField(field : UITextField) {
field.layer.borderColor = UIColor.blackColor().CGColor
field.layer.borderWidth = 2
field.layer.cornerRadius = field.frame.size.height / 2
field.backgroundColor = UIColor.whiteColor()
field.delegate = self
masterView.addSubview(field)
}
styleTextField(textField1)
styleTextField(textField2)
styleTextField(textField3)
styleTextField(textField4)
self.view.addSubview(masterView)
// Do any additional setup after loading the view, typically from a nib.
}
// delegate function of a textfield
func textFieldDidBeginEditing(textField: UITextField) {
focusUserOn(textField) // darken everything else
}
// delegate function of DarkView undarken everything
func tappedDark(view: DarkView) {
guard let superV = view.superview else {
return
}
if let textField = superV.subviews.last as? UITextField {
textField.resignFirstResponder() // also stop editing
}
view.removeFromSuperview()
}
func focusUserOn(textfield: UITextField) {
guard let superV = textfield.superview else {
return
}
let darkArea = DarkView(frame: superV.bounds)
darkArea.delegate = self
superV.addSubview(darkArea)// add DarkView (everything is dark now)
superV.bringSubviewToFront(textfield) // bring the textview back to the front.
}
}
simple subclass of UIView with a gesture recogniser
class DarkView : UIView, UIGestureRecognizerDelegate {
weak var delegate : DarkViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.4)
let tap = UITapGestureRecognizer(target: self, action: Selector("tapped"))
tap.delegate = self
self.addGestureRecognizer(tap)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("deinit dark")
}
func tapped() {
guard let del = self.delegate else {
return
}
del.tappedDark(self)
}
}
Protocol to pass the DarkView when it is tapped to a VC
protocol DarkViewDelegate : class {
func tappedDark(view:DarkView)
}
After thought
func textFieldDidEndEditing(textField: UITextField) {
guard let superV = view.superview else {
return
}
for subview in superV.subviews {
if subview is DarkView {
subview.removeFromSuperview()
}
}
}
Yes, you have the right idea. Add your UIView (let's call it darkeningView) to your storyboard and set it's background color to 50% opaque black. Position it where you want it and add constraints that position it there.
You can also attach a tap gesture recognizer to the darkeningView in IB and set up it's delegate. (You will probably need to set userInteractionEnabled = true on the darkeningView so that it responds to taps.
Add an IBOutlet to your view. In IB, set it to hidden = true.
In your code, when you activate the text field for editing, also set your darkeningView.hidden = false.

Can a UIView subclass create other custom view instances?

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

Resources