How to animate a UILabel that resizes in parallel with its container view - ios

I am trying to animate a multi-line label inside a UIView. In the container view, the width of the label is relative to the bounds. When the container view is animated, the label jumps to the final state and then the container resizes. How can I instead animate the right side of the text to be continuously pinned to the right edge of the container view as it grows larger?
class ViewController: UIViewController {
var container: ContainerView = ContainerView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(container)
container.frame = CGRect(x: 0, y: 0, width: 150, height: 150)
container.center = view.center
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut) {
self.container.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
self.container.center = self.view.center
self.container.layoutIfNeeded()
}
}
}
}
class ContainerView: UIView {
let label: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.text = "foo bar foo bar foo bar foo bar foo bar foo bar foo foo bar foo bar foo bar foo bar foo bar foo bar foo"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .purple
addSubview(label)
}
override func layoutSubviews() {
super.layoutSubviews()
let size = label.sizeThatFits(CGSize(width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude))
label.frame = CGRect(x: 0, y: 0, width: self.bounds.width, height: size.height)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

As you've seen, when we change the width of a label UIKit re-calculates the word wrapping immediately.
When we do something like this:
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut) {
self.container.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
self.container.center = self.view.center
self.container.layoutIfNeeded()
}
UIKit sets the width and then animates it. So, as soon as the animation starts, the word wrapping gets set to the "destination" width.
One way to animate the word wrap changes would be to create an animation loop, using small point-size changes.
That works-ish, with two problems:
Using a UILabel, we get vertical shifting (because the text is vertically centered in a label), and
If we make the incremental size changes small, it's smooth but slow. If we make the incremental changes large, it's quick but "jerky."
To solve the first problem, we can use a UITextView, subclassed to work like a top-aligned UILabel. Here's an example:
class MyTextViewLabel: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() -> Void {
isScrollEnabled = false
isEditable = false
isSelectable = false
textContainerInset = UIEdgeInsets.zero
textContainer.lineFragmentPadding = 0
}
}
Not much we can do about the second problem, other than experiment with the width-increment value.
Here's a complete example to look at and play with (using the above MyTextViewLabel class). Note that I'm also using auto-layout / constraints instead of explicit frames:
class MyContainerView: UIView {
let label: MyTextViewLabel = {
let label = MyTextViewLabel()
label.text = "Let's use some readable text for this example. It will make the wrapping changes look more natural than using a bunch of repeating three-character \"words.\""
// let's set the font to the default UILabel font
label.font = .systemFont(ofSize: 17.0)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
clipsToBounds = true
backgroundColor = .purple
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// let's inset the "label" by 4-points so we can see the purple view frame
label.topAnchor.constraint(equalTo: topAnchor, constant: 4.0),
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4.0),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4.0),
// if we want the bottom text to be "clipped"
// don't set the bottom anchor
//label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4.0),
])
label.backgroundColor = .yellow
}
}
class LabelWrapAnimVC: UIViewController {
// for this example
let startWidth: CGFloat = 150.0
let targetWidth: CGFloat = 200.0
// number of points to increment in each loop
// play with this value...
// 1-point produces a very smooth result, but the total animation time will be slow
// 5-points seems "reasonable" (looks smoother on device than on simulator)
let loopIncrement: CGFloat = 5.0
// total amount of time for the animation
let loopTotalDuration: TimeInterval = 2.0
// each loop anim duration - will be calculated
var loopDuration: TimeInterval = 0
let container: MyContainerView = MyContainerView()
var cWidth: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(container)
container.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
cWidth = container.widthAnchor.constraint(equalToConstant: startWidth)
NSLayoutConstraint.activate([
container.centerXAnchor.constraint(equalTo: g.centerXAnchor),
container.centerYAnchor.constraint(equalTo: g.centerYAnchor),
container.heightAnchor.constraint(equalTo: container.widthAnchor),
cWidth,
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
doAnim()
}
func animLoop() {
cWidth.constant += loopIncrement
// in case we go over the target width
cWidth.constant = min(cWidth.constant, targetWidth)
UIView.animate(withDuration: loopDuration, animations: {
self.view.layoutIfNeeded()
}, completion: { _ in
if self.cWidth.constant < self.targetWidth {
self.animLoop()
} else {
// maybe do something when animation is done
}
})
}
func doAnim() {
// reset width to original
cWidth.constant = startWidth
// calculate loop duration based on size difference
let numPoints: CGFloat = targetWidth - startWidth
let numLoops: CGFloat = numPoints / loopIncrement
loopDuration = loopTotalDuration / numLoops
DispatchQueue.main.async {
self.animLoop()
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
doAnim()
}
}
I don't know if this will be suitable for your target usage, but it's at least worth a look.

Related

inputAccessoryView, API error? _UIKBCompatInputView? UIViewNoIntrinsicMetric, simple code, can't figure out

Help me in one of the two ways maybe:
How to solve the problem? or
How to understand the error message?
Project summary
So I'm learning about inputAccessoryView by making a tiny project, which has only one UIButton. Tapping the button summons the keyboard with inputAccessoryView which contains 1 UITextField and 1 UIButton. The UITextField in the inputAccessoryView will be the final firstResponder that is responsible for the keyboard with that inputAccessoryView
The error message
API error: <_UIKBCompatInputView: 0x7fcefb418290; frame = (0 0; 0 0); layer = <CALayer: 0x60000295a5e0>> returned 0 width, assuming UIViewNoIntrinsicMetric
The code
is very straightforward as below
The custom UIView is used as inputAccessoryView. It installs 2 UI outlets, and tell responder chain that it canBecomeFirstResponder.
class CustomTextFieldView: UIView {
let doneButton:UIButton = {
let button = UIButton(type: .close)
return button
}()
let textField:UITextField = {
let textField = UITextField()
textField.placeholder = "placeholder"
return textField
}()
required init?(coder: NSCoder) {
super.init(coder: coder)
initSetup()
}
override init(frame:CGRect) {
super.init(frame: frame)
initSetup()
}
convenience init() {
self.init(frame: .zero)
}
func initSetup() {
addSubview(doneButton)
addSubview(textField)
}
func autosizing(to vc: UIViewController) {
frame = CGRect(x: 0, y: 0, width: vc.view.frame.size.width, height: 40)
let totalWidth = frame.size.width - 40
doneButton.frame = CGRect(x: totalWidth * 4 / 5 + 20,
y: 0,
width: totalWidth / 5,
height: frame.size.height)
textField.frame = CGRect(x: 20,
y: 0,
width: totalWidth * 4 / 5,
height: frame.size.height)
}
override var canBecomeFirstResponder: Bool { true }
override var intrinsicContentSize: CGSize {
CGSize(width: 400, height: 40)
} // overriding this variable seems to have no effect.
}
Main VC uses the custom UIView as inputAccessoryView. The UITextField in the inputAccessoryView becomes the real firstResponder in the end, I believe.
class ViewController: UIViewController {
let customView = CustomTextFieldView()
var keyboardShown = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
customView.autosizing(to: self)
}
#IBAction func summonKeyboard() {
print("hello")
keyboardShown = true
self.becomeFirstResponder()
customView.textField.becomeFirstResponder()
}
override var canBecomeFirstResponder: Bool { keyboardShown }
override var inputAccessoryView: UIView? {
return customView
}
}
I've seen people on the internet says this error message will go away if I run on a physical phone. I didn't go away when I tried.
I override intrinsicContentSize of the custom view, but it has no effect.
The error message shows twice together when I tap summon.
What "frame" or "layer" does the error message refer to? Does it refer to the custom view's frame and layer?
If we use Debug View Hierarchy we can see that _UIKBCompatInputView is part of the (internal) view hierarchy of the keyboard.
It's not unusual to see constraint errors / warnings with internal views.
Since frame and/or intrinsic content size seem to have no effect, I don't think it can be avoided (nor does it seem to need to be).
As a side note, you can keep the "Done" button round by using auto-layout constraints. Here's an example:
class CustomTextFieldView: UIView {
let textField: UITextField = {
let tf = UITextField()
tf.font = .systemFont(ofSize: 16)
tf.autocorrectionType = .no
tf.returnKeyType = .done
tf.placeholder = "placeholder"
// textField backgroundColor so we can see its frame
tf.backgroundColor = .yellow
return tf
}()
let doneButton:UIButton = {
let button = UIButton(type: .close)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
autoresizingMask = [.flexibleHeight, .flexibleWidth]
[doneButton, textField].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
}
NSLayoutConstraint.activate([
// constrain doneButton
// Trailing: 20-pts from trailing
doneButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
// Top and Bottom 8-pts from top and bottom
doneButton.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
doneButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
// Width equal to default height
// this will keep the button round instead of oval
doneButton.widthAnchor.constraint(equalTo: doneButton.heightAnchor),
// constrain textField
// Leading: 20-pts from leading
textField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
// Trailing: 8-pts from doneButton leading
textField.trailingAnchor.constraint(equalTo: doneButton.leadingAnchor, constant: -8.0),
// vertically centered
textField.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
}
class CustomTextFieldViewController: UIViewController {
let customView = CustomTextFieldView()
var keyboardShown = false
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func summonKeyboard() {
print("hello")
keyboardShown = true
self.becomeFirstResponder()
customView.textField.becomeFirstResponder()
}
override var canBecomeFirstResponder: Bool { keyboardShown }
override var inputAccessoryView: UIView? {
return customView
}
}

inputAccessoryView not respecting safeAreaLayoutGuide when keyboard is collapsed

I am trying to get an inputAccessoryView working correctly. Namely, I want to be able to display, in this case, a UIToolbar in two possible states:
Above the keyboard - standard and expected behavior
At the bottom of the screen when the keyboard is dismissed (e.g. command + K in the simulator) - and in such instances, have the bottomAnchor respect the bottom safeAreaLayoutGuide.
I've researched this topic extensively but every suggestion I can find has a bunch of workarounds that don't seem to align with Apple engineering's suggested solution. Based on an openradar ticket, Apple engineering proposed this solution be approached as follows:
It’s your responsibility to respect the input accessory view’s
safeAreaInsets. We designed it this way so developers could provide a
background view (i.e., see Safari’s Find on Page input accessory view)
and lay out the content view with respect to safeAreaInsets. This is
fairly straightforward to accomplish. Have a view hierarchy where you
have a container view and a content view. The container view can have
a background color or a background view that encompasses its entire
bounds, and it lays out it’s content view based on safeAreaInsets. If
you’re using autolayout, this is as simple as setting the content
view’s bottomAnchor to be equal to it’s superview’s
safeAreaLayoutGuide.
The link for the above is: http://www.openradar.me/34411433
I have therefore constructed a simple xCode project (iOS App template) that has the following code:
class ViewController: UIViewController {
var field = UITextField()
var containerView = UIView()
var contentView = UIView()
var toolbar = UIToolbar()
override func viewDidLoad() {
super.viewDidLoad()
// TEXTFIELD
field = UITextField(frame: CGRect(x: 20, y: 100, width: view.frame.size.width, height: 50))
field.placeholder = "Enter name..."
field.backgroundColor = .secondarySystemBackground
field.inputAccessoryView = containerView
view.addSubview(field)
// CONTAINER VIEW
containerView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: 50)
containerView.backgroundColor = .systemYellow
containerView.translatesAutoresizingMaskIntoConstraints = false
// CONTENT VIEW
contentView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: 50)
contentView.backgroundColor = .systemPink
contentView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(contentView)
// TOOLBAR
toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 50))
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(didTapDone))
toolbar.setItems([flexibleSpace, doneButton], animated: true)
toolbar.backgroundColor = .systemGreen
toolbar.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(toolbar)
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: containerView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: contentView.superview!.safeAreaLayoutGuide.bottomAnchor),
toolbar.topAnchor.constraint(equalTo: contentView.topAnchor),
toolbar.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
toolbar.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
toolbar.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
}
#objc private func didTapDone() {
print("done tapped")
}
}
The result works whilst the keyboard is visible but doesn't once the keyboard is dimissed:
I've played around with the heights of the various views with mixed results and making the container view frame height larger (e.g. 100), does show the toolbar when the keyboard is collapsed, it also makes the toolbar too tall for when the keyboard is visible.
Clearly I'm making some auto layout constraint issues but I can't work out and would appreciate any feedback that provides a working solution aligned with Apple's recommendation.
Thanks in advance.
In my case I use the following approach:
import UIKit
extension UIView {
func setDimensions(height: CGFloat, width: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
heightAnchor.constraint(equalToConstant: height).isActive = true
widthAnchor.constraint(equalToConstant: width).isActive = true
}
func setHeight(_ height: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
class CustomTextField: UITextField {
override init(frame: CGRect) {
super.init(frame: frame)
}
convenience init(placeholder: String) {
self.init(frame: .zero)
configureUI(placeholder: placeholder)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureUI(placeholder: String) {
let spacer = UIView()
spacer.setDimensions(height: 50, width: 12)
leftView = spacer
leftViewMode = .always
borderStyle = .none
textColor = .white
keyboardAppearance = .dark
backgroundColor = UIColor(white: 1, alpha: 0.1)
setHeight(50)
attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [.foregroundColor: UIColor(white: 1, alpha: 0.75)])
}
}
I was able to achieve the effect by wrapping the toolbar (chat input bar in my case) and constraining it top/right/left + bottom to safe area of the wrapper.
I'll leave an approximate recipe below.
In your view controller:
override var inputAccessoryView: UIView? {
keyboardHelper
}
override var canBecomeFirstResponder: Bool {
true
}
lazy var keyboardHelper: InputBarWrapper = {
let wrapper = InputBarWrapper()
let inputBar = InputBar()
helper.addSubview(inputBar)
inputBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
inputBar.topAnchor.constraint(equalTo: helper.topAnchor),
inputBar.leftAnchor.constraint(equalTo: helper.leftAnchor),
inputBar.bottomAnchor.constraint(equalTo:
helper.safeAreaLayoutGuide.bottomAnchor),
inputBar.rightAnchor.constraint(equalTo: helper.rightAnchor),
])
return wrapper
}()
Toolbar wrapper subclass:
class InputBarWrapper: UIView {
var desiredHeight: CGFloat = 0 {
didSet { invalidateIntrinsicContentSize() }
}
override var intrinsicContentSize: CGSize {
CGSize(width: 0, height: desiredHeight)
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
override init(frame: CGRect) {
super.init(frame: frame);
autoresizingMask = .flexibleHeight
backgroundColor = UIColor.systemGreen.withAlphaComponent(0.2)
}
}

how to remove safe area from UIView Programmatically in Swift?

This is a custom view, this view creates a square with a given frame with the background color. I am adding a custom view to a subview, the view appears properly. But I am not able to cover the bottom safe area, anyone can help me to remove the safe area from bottom Programmatically.
class CustomView: UIView {
override var frame: CGRect {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.isOpaque = false
}
override func draw(_ rect: CGRect) {
UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.7).setFill()
UIRectFill(rect)
let square = UIBezierPath(rect: CGRect(x: 200, y: rect.size.height/2 - 150/2, width: UIScreen.main.bounds.size.width - 8, height: 150))
let dashPattern : [CGFloat] = [10, 4]
square.setLineDash(dashPattern, count: 2, phase: 0)
UIColor.white.setStroke()
square.lineWidth = 5
square.stroke()
}
}
Consider the following example:
class ViewController: UIViewController {
private let myView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
configureCustomView()
}
private func configureCustomView() {
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
myView.backgroundColor = .systemPurple
NSLayoutConstraint.activate([
myView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
myView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
myView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
myView.heightAnchor.constraint(equalToConstant: 200)
])
}
}
Result:
If you don't want to go over the safe area, then you could use myView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) inside NSLayoutConstraint.activate([...]).
So you actually don't have to remove the SafeArea, because you can simply ignore them if you want so...
In case you want to do this fast. (Not programatically)
Open storyboard.
Select your UIView.
Safe Area should be unselected.

View with multiple layers dont relese memory

I create custom View:
final class Clock: UIView {
lazy var hourArrow: CALayer = {
let layer = CALayer()
layer.contents = #imageLiteral(resourceName: "hourArrow").cgImage
layer.contentsGravity = kCAGravityResizeAspect
return layer
}()
lazy var subLayers = [hourArrow,minuteArrow,centerDial,clockDial,clockArrow,block]
override init(frame: CGRect) {
super.init(frame: frame)
subLayers.forEach { (l) in
layer.addSublayer(l)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
subLayers.forEach({ l in
let rect = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
l.frame = rect
})
}
}
and i use it ViewController:
class ClockViewController: UIViewController {
lazy var clock: Clock = {
let clock = Clock(frame: .zero)
clock.translatesAutoresizingMaskIntoConstraints = false
return clock
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(clock)
let safeArea = view.safeAreaLayoutGuide
clock.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 40).isActive = true
clock.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 40).isActive = true
clock.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -40).isActive = true
clock.heightAnchor.constraint(equalToConstant: 100).isActive = true
// Do any additional setup after loading the view.
}
}
then i move to this view controller, memory up to 10 MB with this layers.
When i dismiss VC, memory don't release but deinit was called.
(I must remove CALayer variables to post this, all of layers are create like first)
Any Ideas?

Swift: Custom view hierarchy

I am attempting to build a cinema ticketing app and I need help with my view hierarchy. At the moment my cinemaView does not get added to the VC.
Here is how I attempt to do it. I have a custom seatView that has two properties (isVacant and seatNumber) and a custom cinemaView that has [seatView] as a property (well different cinema has different seatings). My code as such:
//In my viewController
class ViewController: UIViewController, UIScrollViewDelegate {
let scrollView = UIScrollView()
let cinema: CinemaView = {
let v = CinemaView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 10.0
scrollView.zoomScale = scrollView.minimumZoomScale
scrollView.delegate = self
scrollView.isScrollEnabled = true
scrollView.translatesAutoresizingMaskIntoConstraints = false
let seat1 = SeatView()
seat1.isVacant = false
seat1.seatNumber = "2A"
let seat2 = SeatView()
seat2.isVacant = true
seat2.seatNumber = "3B"
cinema.seats = [seat1, seat2]
view.addSubview(scrollView)
scrollView.addSubview(cinema)
let views = ["scrollView": scrollView, "v": cinema]
let screenHeight = view.frame.height
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[scrollView(\(screenHeight / 2))]", options: NSLayoutFormatOptions(), metrics: nil, views: views))
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[scrollView]|", options: NSLayoutFormatOptions(), metrics: nil, views: views))
scrollView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-60-[v(50)]", options: NSLayoutFormatOptions(), metrics: nil, views: views))
scrollView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[v]|", options: NSLayoutFormatOptions(), metrics: nil, views: views))
}
//At my custom cinemaView
class CinemaView: UIView {
var seats = [SeatView]()
var xPos: Int = 0
let cinemaView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(cinemaView)
cinemaView.backgroundColor = .black
let views = ["v": cinemaView]
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[v]|", options: NSLayoutFormatOptions(), metrics: nil, views: views))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[v]|", options: NSLayoutFormatOptions(), metrics: nil, views: views))
for seat in seats {
cinemaView.addSubview(seat)
seat.frame = CGRect(x: xPos, y: 0, width: 20, height: 20)
xPos += 8
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//At my custom seatView
class SeatView: UIView {
var isVacant: Bool?
var seatNumber: String?
let seatView: UIView = {
let v = UIView()
v.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
v.layer.cornerRadius = 5
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(seatView)
seatView.backgroundColor = setupBackgroundColor()
}
func setupBackgroundColor() -> UIColor {
if let isVacant = isVacant {
if isVacant {
return UIColor.green
} else {
return UIColor.black
}
} else {
return UIColor.yellow
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
My code does not seem to add the cinemaView to my VC. Could anyone point me where have I gone wrong? Or perhaps even advice if this method is suitable for this application? Thanks.
You need to give the frames while creating any UIView.
override init(frame: CGRect) will be called when you specify the frame of your custom UIView.
I have created a similar hierarchy as yours as an example. Have a look at it.
Also xPos must be such that it does not overlap the previous SeatView, i.e (new xPos + previous SeatView width).
Also in your SeatView and CinemaView you are adding a UIView inside another UIView which is kind of redundant. You don't need to do that.
Example:
class ViewController: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
let seat1 = SeatView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
let seat2 = SeatView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
let cinema = CinemaView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
cinema.seats = [seat1, seat2]
self.view.addSubview(cinema)
}
}
class CinemaView: UIView
{
var seats = [SeatView](){
didSet{
for seat in seats
{
seat.frame.origin.x = xPos
xPos += 100
self.addSubview(seat)
}
}
}
var xPos: CGFloat = 0
override init(frame: CGRect)
{
super.init(frame: frame)
self.backgroundColor = .black
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
}
class SeatView: UIView
{
override init(frame: CGRect)
{
super.init(frame: frame)
self.backgroundColor = .red
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
}
Well, you are doing several things wrong here...
You are subclassing UIView, but in your custom view you are adding a UIView as a subview, and trying to treat that as your actual view.
In your CinemaView class, you are looping your array of "seats" ... but you are doing so before assigning the array, so no seats will ever be created.
Similarly, in SeatView, you are trying to set the background color based on the .isVacant property, but you are doing so before the property is set.
You are trying to use a UIScrollView but you do not have the constraints set up correctly.
Setting constraints with Visual Format may feel convenient or easy, but it has limits. In your specific case, you want your scroll view to be 1/2 the height of your main view. With VFL, you have to calculate and explicitly set the constant, which will also have to be re-calculated any time the frame changes. Using the scroll view's .heightAnchor.constraint, setting it equal to the .heightAnchor of the view, and setting multiplier: 0.5 gets you 50% without any calculations.
So, lots to understand and lots to think about. I've made some edits to your original code. This should be considered a "starting point" for you to learn from, not "drop-in finished code":
class CinemaViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let sv = UIScrollView()
sv.minimumZoomScale = 1.0
sv.maximumZoomScale = 10.0
sv.zoomScale = sv.minimumZoomScale
sv.isScrollEnabled = true
sv.translatesAutoresizingMaskIntoConstraints = false
return sv
}()
let cinema: CinemaView = {
let v = CinemaView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.black
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// set our background to yellow, so we can see where it is
view.backgroundColor = .yellow
// create 2 "SeatView" objects
let seat1 = SeatView()
seat1.isVacant = false
seat1.seatNumber = "2A"
let seat2 = SeatView()
seat2.isVacant = true
seat2.seatNumber = "3B"
// set the array of "seats" in the cinema view object
cinema.seats = [seat1, seat2]
// assign scroll view delegate
scrollView.delegate = self
// set scroll view background color, so we can see it
scrollView.backgroundColor = .blue
// add the scroll view to this view
view.addSubview(scrollView)
// pin scrollView to top, leading and trailing, with 8-pt padding
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8.0).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8.0).isActive = true
// set scrollView height to 50% of view height
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5).isActive = true
// add cinema view to the scroll view
scrollView.addSubview(cinema)
// pin cinema to top and leading of scrollView, with 8.0-pt padding
cinema.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 8.0).isActive = true
cinema.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 8.0).isActive = true
// set the width of cinema to scrollView width -16.0 (leaves 8.0-pts on each side)
cinema.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -16.0).isActive = true
// cinema height set to constant of 60.0 (for now)
cinema.heightAnchor.constraint(equalToConstant: 60.0).isActive = true
// in order to use a scroll view, its .contentSize must be defined
// so, use the trailing and bottom anchor constraints of cinema to define the .contentSize
cinema.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 0.0).isActive = true
cinema.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0.0).isActive = true
}
}
//At my custom cinemaView
class CinemaView: UIView {
// when the seats property is set,
// remove any existing seat views
// and add a new seat view for each seat
// Note: eventually, this will likely be done with Stack Views and / or constraints
// rather than calculated Rects
var seats = [SeatView]() {
didSet {
for v in self.subviews {
v.removeFromSuperview()
}
var seatRect = CGRect(x: 0, y: 0, width: 20, height: 20)
for seat in seats {
self.addSubview(seat)
seat.frame = seatRect
seatRect.origin.x += seatRect.size.width + 8
}
}
}
// we're not doing anything on init() - yet...
// un-comment the following if you need to add setup code
// see the similar functionality in SeatView class
/*
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
*/
// perform any common setup tasks here
func commonInit() -> Void {
//
}
}
//At my custom seatView
class SeatView: UIView {
// change the background color when .isVacant property is set
var isVacant: Bool = false {
didSet {
if isVacant {
self.backgroundColor = UIColor.green
} else {
self.backgroundColor = UIColor.red
}
}
}
// not used currently
var seatNumber: String?
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
// perform any common setup tasks here
func commonInit() -> Void {
self.layer.cornerRadius = 5
}
}

Resources