Adding Subclasses to UIView in Swift - ios

I am unable to add a subclass to my parent UIView class. I am trying to build a BOOK class and have various UIView and UIImageView classes to build the covers and the pages. I get an error when adding the subclass to SELF. Would love some insight. PS - total swift noob
//book
class bookview : UIView {
var cover: UIView!
var backcover: UIView!
var page: UIImageView!
init (pages: Int) {
//backcover cover
backcover = UIView(frame: CGRect(x: 200, y: 200, width: bookwidth, height: bookheight))
backcover.backgroundColor = UIColor.blue
self.addSubview(backcover) //ERROR HERE
//pages
for i in 0 ..< pages {
page = UIImageView(frame: CGRect(x: bookwidth * i/10, y: bookheight * i/10, width: bookwidth, height: bookheight))
page.backgroundColor = UIColor.red
self.addSubview(page) //ERROR HERE
}
//front cover
cover = UIView(frame: CGRect(x: 0, y: 0, width: bookwidth, height: bookheight))
cover.backgroundColor = UIColor.blue
self.addSubview(cover) //ERROR HERE
super.init(frame: CGRect(x: 0, y: 0, width: bookwidth, height: bookheight))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//add book
let book = bookview(pages: 3)

The issue is that you can't call methods on self in the initializer until there is a self to call them on. There is no established value for self until you have called the superclass initializer. In other words, how does a subclass of UIView know how to "addSubview" when it has not yet been initialized as a UIView yet?
So, in your code example, just move the line:
super.init(frame: CGRect(x: 0, y: 0, width: bookwidth, height: bookheight))
to be before any time you call self.addSubview()

addSubview() is a method on UIView. UIView is your view's superclass. You cannot call methods on a superclass until it has been fully initialized.
To fix this, call super.init(frame:) earlier in your own init() function (before you call addSubview()).

Related

How to re-draw line connected between moveable two UIView

I would like to re-draw the line between two movable UIView, depending on the position of UIView.
So I found this. However, this method uses "Pan Gesture" to re-draw lines, depending on the position of the gesture.
Also found are setNeedsDisplay(), but this is a request to re-draw, not a real-time event function.
The way I want to find it is not to use gestures to redraw lines, but to re-draw lines in real time.
In a little bit more detail, I applied "UIColisionBehavior" to all the UIVviews I created. The UIView applied changes position as they are struck, and depending on the changed position, the line is being redrawn.
As if the UIView were moving in this way, the connected line was redrawn according to where it was moved:
Below is the code I'm experimenting with in the Playground. When you execute the code, you can see that the first connected purple line is connected to the falling UIView and does not fall off:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MoveAbleView : UIView {
var outGoingLine : CAShapeLayer?
var inComingLine : CAShapeLayer?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func lineTo(connectedView: MoveAbleView) -> CAShapeLayer {
let path = UIBezierPath()
path.move(to: self.center)
path.addLine(to: connectedView.center)
let line = CAShapeLayer()
line.path = path.cgPath
line.lineWidth = 5
line.strokeColor = UIColor.purple.cgColor
connectedView.inComingLine = line
outGoingLine = line
return line
}
}
class MyViewController : UIViewController {
var dynamicAnimator = UIDynamicAnimator()
var collisionBehavior = UICollisionBehavior()
var gravityBehavior = UIGravityBehavior()
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
let viw = MoveAbleView(frame: CGRect(x: 100, y: 200, width: 50, height: 50))
viw.backgroundColor = UIColor.red
self.view.addSubview(viw)
let viw2 = MoveAbleView(frame: CGRect(x: 300, y: 100, width: 50, height: 50))
viw2.backgroundColor = UIColor.orange
self.view.addSubview(viw2)
let gravityViw = MoveAbleView(frame: CGRect(x: 100, y: 0, width: 50, height: 50))
gravityViw.backgroundColor = UIColor.green
self.view.addSubview(gravityViw)
let gravityViw2 = MoveAbleView(frame: CGRect(x: 300, y: -200, width: 50, height: 50))
gravityViw2.backgroundColor = UIColor.blue
self.view.addSubview(gravityViw2)
collisionBehavior.addItem(viw)
collisionBehavior.addItem(viw2)
collisionBehavior.addItem(gravityViw)
collisionBehavior.addItem(gravityViw2)
gravityBehavior.addItem(gravityViw)
gravityBehavior.addItem(gravityViw2)
dynamicAnimator.addBehavior(collisionBehavior)
dynamicAnimator.addBehavior(gravityBehavior)
self.view.layer.addSublayer(viw.lineTo(connectedView: viw2))
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
When UIView strikes and moves, how do you redraw a connected line in real time?
Actually, all what you need is another UIView to represent the lines and employ attachmentBehaviors. For instance, there is a line between two attached objects.
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
dynamicAnimator = UIDynamicAnimator(referenceView: view)
let viw = MoveAbleView(frame: CGRect(x: 100, y: 200, width: 50, height: 50))
viw.backgroundColor = UIColor.red
self.view.addSubview(viw)
let viw2 = MoveAbleView(frame: CGRect(x: 300, y: 200, width: 50, height: 50))
viw2.backgroundColor = UIColor.orange
self.view.addSubview(viw2)
let gravityViw = MoveAbleView(frame: CGRect(x: 100, y: 0, width: 50, height: 50))
gravityViw.backgroundColor = UIColor.green
self.view.addSubview(gravityViw)
let line1 = MoveAbleView(frame: CGRect(x: 125, y: 225, width: 200, height: 10))
line1.backgroundColor = UIColor.purple
self.view.addSubview(line1)
let l1 = UIAttachmentBehavior.init(item: viw, offsetFromCenter: UIOffset.zero, attachedTo: line1, offsetFromCenter: UIOffset.init(horizontal: -100, vertical: 0))
let l2 = UIAttachmentBehavior.init(item: viw2, offsetFromCenter: UIOffset.zero, attachedTo: line1, offsetFromCenter: UIOffset.init(horizontal: 100, vertical: 0))
collisionBehavior.addItem(viw)
collisionBehavior.addItem(viw2)
collisionBehavior.addItem(gravityViw)
gravityBehavior.addItem(gravityViw)
dynamicAnimator.addBehavior(l1)
dynamicAnimator.addBehavior(l2)
dynamicAnimator.addBehavior(collisionBehavior)
dynamicAnimator.addBehavior(gravityBehavior)
}
}

Unable to properly animate the frame of a subview within a parent view

I am trying to animate the frame change of a subview within a view. Within the animation block I am setting the size of the subview to half its original width and height. However, when I run the code, the subview unexpectedly started with a larger size and shrunk to its original size instead of becoming half its original size. I am using the code below:
My custom view:
import UIKit
class TestView: UIView {
var testSubview: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.yellow
testSubview = UIView(frame: .zero)
testSubview.backgroundColor = UIColor.red
addSubview(testSubview)
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
override func layoutSubviews() {
super.layoutSubviews()
testSubview.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
}
func testAnimate() {
UIView.animate(withDuration: 3, delay: 0, options: [], animations: { [unowned self] in
self.testSubview.bounds.size = CGSize(width: 50, height: 50)
}, completion: nil)
}
}
My view controller:
import UIKit
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let testView = TestView()
view.addSubview(testView)
testView.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
testView.center = view.center
testView.layoutIfNeeded()
testView.testAnimate()
}
}
Am I doing something wrongly?
The problem is that your view gets laid out two times, you can check that by printing a debug message inside layoutSubviews().
I would suggest something like this, setting a size variable and use that in both layoutSubviews() and testAnimate():
class TestView: UIView {
var testSubview: UIView!
var mySize = CGSize(width: 100, height: 100)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.yellow
testSubview = UIView(frame: .zero)
testSubview.backgroundColor = UIColor.red
addSubview(testSubview)
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
override func layoutSubviews() {
super.layoutSubviews()
testSubview.frame = CGRect(x: 0, y: 0, width: mySize.width, height: mySize.height)
}
func testAnimate() {
UIView.animate(withDuration: 3, delay: 0, options: [], animations: { [unowned self] in
self.mySize = CGSize(width: 50, height: 50)
self.testSubview.bounds.size = self.mySize
}, completion: nil)
}
}
EDIT: Reformulating my whole answer because there are many things I feel should be changed here.
first of all the difference between setting the bounds of a view and it's frame.
let view = UIView()
view.frame = CGRect(x: 0, y: 0, width: 200, height: 200) // This will set the position and size this view will take relative to it's future parents view
view.bounds = CGRect(x: 50, y: 50, width: 100, height: 100) // This will set position and size of it's own content (i.e. it's background) relative to it's own view
Then comes your testView class variables. Unless you are certain that a variable won't be nil don't use UIView! as the typing, you would end up missing some initialisers and thus your class would not work consistently.
Consider init methods as default values for your variables and you can simply alter them afterwards.
import UIKit
class TestView: UIView {
var testSubview: UIView
override init(frame: CGRect) {
// Initialise your variables before calling super
testSubview = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
testSubview.backgroundColor = UIColor.red
super.init(frame: frame)
// Once super is called you can start using class methods or variable such as addSubview or backgroundColor
backgroundColor = UIColor.yellow
addSubview(testSubview)
}
required init?(coder aDecoder: NSCoder) {
testSubview = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
testSubview.backgroundColor = UIColor.blue
super.init(coder: aDecoder)
backgroundColor = UIColor.yellow
addSubview(testSubview)
}
}
now finally to the animation part
import UIKit
class TestView: UIView {
var testSubview: UIView
func testAnimation() {
UIView.animate(withDuration: 3, animations: {
self.testSubview.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) // Simply make the view half its size over 3 seconds
}) { (_) in
UIView.animate(withDuration: 3, animations: {
self.testSubview.transform = CGAffineTransform(scaleX: 1, y: 1) // Upon completion, resize the view back to it's original size
})
}
}
}
or if you really want to use CGRect to do animation on it's position
import UIKit
class TestView: UIView {
var testSubview: UIView
func testAnimation() {
UIView.animate(withDuration: 3, animations: {
self.testSubview.frame = CGRect(x: 0, y: 0, width: 50, height: 50) // use frame not bounds to resize and reposition the frame
}) { (_) in
UIView.animate(withDuration: 3, animations: {
self.testSubview.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
})
}
}
}
you can test the effect of changing the bounds attribute as such
import UIKit
class ViewController: UIViewController {
var testView: TestView!
override func viewDidLoad() {
testView = TestView(frame: CGRect(origin: .zero, size: CGSize(width: 200, height: 200)))
testView.bounds = CGRect(x: 50, y: 50, width: 100, height: 100)
testView.clipsToBounds = true
view.addSubview(testView)
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
testView.testAnimation()
}
}
simply play around with clipsToBounds, testView.bounds, testView.subView.frame and testView = TestView(frame: CGRect(x:y:width:height) to see all of this

Use of 'self' in property access 'frame' before super.init initializes self

I have this code
import UIKit
class CardView: UIView {
#IBOutlet var imageView: UIImageView!
init(imageView: UIImageView) {
self.imageView = imageView
super.init(frame: CGRect(x: 0, y:0, width: self.frame.size.width, height: self.frame.size.height))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I get an error at the line:
super.init(frame: CGRect(x: 0, y:0, width: self.frame.size.width, height: self.frame.size.height))
The error says Use of 'self' in property access 'frame' before super.init initializes self
I don't have any idea how to solve this.
Please bare in mind I am from an objective - C background and recently started learning swift.
You must call super.init() before accessing self within a init() method:
init(imageView: UIImageView) {
self.imageView = imageView /*you are accessing self here before calling super init*/
super.init(frame: CGRect(x: 0, y:0, width: self.frame.size.width /* here also*/, height: self.frame.size.height))
}
Change it to:
init(imageView: UIImageView) {
super.init(frame: CGRect(origin: CGPoint.zero, size: imageView.frame.size))
self.imageView = imageView
}

Custom UIView not showing anything

I have created a custom UIView :
import UIKit
class IndicatorView: UIView {
var image_view_indicator: UIImageView!
var label_indicator: UILabel!
required init(coder decoder: NSCoder) {
super.init(coder: decoder)!
}
init(frame:CGRect, imageName: String, indicatorValue: String)
{
super.init(frame: frame)
self.frame = frame
image_view_indicator = UIImageView(frame: CGRect(x: 0, y: 0, width: 15, height: 15))
image_view_indicator.image = UIImage(named: imageName)
self.addSubview(image_view_indicator)
self.bringSubviewToFront(image_view_indicator)
label_indicator = UILabel(frame: CGRect(x: image_view_indicator.frame.size.width + 3, y: 0, width: 25, height: 15))
label_indicator.text = indicatorValue
label_indicator.textColor = UIColor.blackColor()
self.addSubview(label_indicator)
self.bringSubviewToFront(label_indicator)
}
}
This is my call from UIViewController :
view_indicator_friends = IndicatorView(frame: view_indicator_friends.frame, imageName: "friend.png", indicatorValue: "20")
I cross checked the IBOutlet connections. They seem to be fine.
But nothing is showing up in the custom view.
When you take your custom view from Xib through IBOutlet connection then your custom init will not invoked that why your image_view_indicator and label_indicator does not created. Thats why your custom view does not showing up properly. One solution is that implement your code under init(frame:CGRect, imageName: String, indicatorValue: String) method in the awakeFromNib method.
override func awakeFromNib() {
let frame = self.frame
image_view_indicator = UIImageView(frame: CGRect(x: 0, y: 0, width: 15, height: 15))
image_view_indicator.image = UIImage(named: imageName)
self.addSubview(image_view_indicator)
self.bringSubviewToFront(image_view_indicator)
label_indicator = UILabel(frame: CGRect(x: image_view_indicator.frame.size.width + 3, y: 0, width: 25, height: 15))
label_indicator.text = indicatorValue
label_indicator.textColor = UIColor.blackColor()
self.addSubview(label_indicator)
self.bringSubviewToFront(label_indicator)
}

Swift pull down insert form

I am lost.
I have a TableViewController that is filled programmatically from a collection. I need to show a form when the tableView is pulled down. Right now I have a InsertControl that contains two textFields.
InsertControl
class InsertControl: UIView {
//MARK: Properties
var name = ""
var quantity = 0
//MARK: Initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let nameTextField = UITextField(frame: CGRect(x: 0, y: 0, width: 260, height: 44))
nameTextField.backgroundColor = UIColor.redColor()
let quantityTextField = UITextField(frame: CGRect(x: 270, y: 0, width: 60, height: 44))
quantityTextField.backgroundColor = UIColor.redColor()
addSubview(nameTextField)
addSubview(quantityTextField)
}
override func intrinsicContentSize() -> CGSize {
return CGSize(width: 350, height: 44)
}
}
then I found that I can put this control in the tableView header, so I have this code in my
tableViewController
let insertControl = InsertControl()
self.tableView.tableHeaderView = insertControl
self.tableView.contentOffset = CGPoint(x: 0, y: 44)
The problem is that I need to give to the InsertControl constructor a NSCoder which I don't have.
After I manage to get the form in the header, how can I pass a further swipe down to the control and make it add the content to the tableView collection?
I hope I was clear enough.
To answer your first question, in InsertControl, use override init(frame: CGRect) instead of required init?(coder aDecoder: NSCoder). It'll look like this:
override init(frame: CGRect) {
super.init(frame: frame)
let nameTextField = UITextField(frame: CGRect(x: 0, y: 0, width: 260, height: 44))
nameTextField.backgroundColor = UIColor.redColor()
let quantityTextField = UITextField(frame: CGRect(x: 270, y: 0, width: 60, height: 44))
quantityTextField.backgroundColor = UIColor.redColor()
addSubview(nameTextField)
addSubview(quantityTextField)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func intrinsicContentSize() -> CGSize {
return CGSize(width: 350, height: 44)
}

Resources