Why doesn't my UILabel in a nested view receive touch events / How can I test the Responder Chain? - uistackview

I have found lots of similar questions about not receiving touch events and I understand that in some cases, writing a custom hitTest function may be required - but I also read that the responder chain will traverse views and viewControllers that are in the hierarchy - and I don't understand why a custom hitTest would be required for my implementation.
I'm looking for an explanation and/or a link to a document that explains how to test the responder chain. This problem is occurring in Xcode 10.2.1.
My scenario (I am not using Storyboard):
I have a mainViewController, that provides a full screen view with an ImageView and a few Labels. I have attached TapGestureRecognizers to the ImageView and one of the labels - and they both work properly.
When I tap the label, I add a child viewController and it's view as a subview to the mainViewController. The view is constrained to cover only the right-half of the screen.
The child viewController contains a vertical stack view that contains 3 arrangedSubviews.
Each arrangedSubview contains a Label and a horizontal StackView.
The horizontal stackView's each contain a View with a Label as a subview.
The Label in the subview sets it's isUserInteractionEnabled flag to True and adds a TapGestureRecognizer.
These are the only objects in the child ViewController that have 'isUserInteractionEnabled' set.
The Label's are nested fairly deep, but since this is otherwise a direct parent/child hierarchy (as opposed to the 2 views belonging to a NavigationController), I would expect the Label's to be in the normal responder chain and function properly. Do the Stack View's change that behavior? Do I need to explicitly set the 'isUserInteractionEnabled' value to False on some of the views? Is there way I can add logging to the ResponderChain so I can see which views it checked and find out where it is being blocked?
After reading this StackOverflow post I tried adding my gesture recognizers in viewDidLayoutSubviews() instead of what's shown below - but they still do not receive tap events.
Thank you in advance to any who can offer advice or help.
Here is the code for the label that is not responding to my tap events and the tap event it should call:
func makeColorItem(colorName:String, bgColor:UIColor, fgColor:UIColor) -> UIView {
let colorNumber:Int = colorLabelDict.count
let colorView:UIView = {
let v = UIView()
v.tag = 700 + colorNumber
v.backgroundColor = .clear
v.contentMode = .center
return v
}()
self.view.addSubview(colorView)
let tapColorGR:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapColor))
let colorChoice: UILabel = {
let l = UILabel()
l.tag = 700 + colorNumber
l.isUserInteractionEnabled = true
l.addGestureRecognizer(tapColorGR)
l.text = colorName
l.textAlignment = .center
l.textColor = fgColor
l.backgroundColor = bgColor
l.font = UIFont.systemFont(ofSize: 24, weight: .bold)
l.layer.borderColor = fgColor.cgColor
l.layer.borderWidth = 1
l.layer.cornerRadius = 20
l.layer.masksToBounds = true
l.adjustsFontSizeToFitWidth = true
l.translatesAutoresizingMaskIntoConstraints = false
l.widthAnchor.constraint(equalToConstant: 100)
return l
}()
colorView.addSubview(colorChoice)
colorChoice.centerXAnchor.constraint(equalTo: colorView.centerXAnchor).isActive = true
colorChoice.centerYAnchor.constraint(equalTo: colorView.centerYAnchor).isActive = true
colorChoice.heightAnchor.constraint(equalToConstant: 50).isActive = true
colorChoice.widthAnchor.constraint(equalToConstant: 100).isActive = true
colorLabelDict[colorNumber] = colorChoice
return colorView
}
#objc func tapColor(sender:UITapGestureRecognizer) {
print("A Color was tapped...with tag:\(sender.view?.tag ?? -1)")
if let cn = sender.view?.tag {
colorNumber = cn
let v = colorLabelDict[cn]
if let l = (v?.subviews.first as? UILabel) {
print("The \(l.text) label was tapped.")
}
}
}

It looks like the main reason you're not getting a tap recognized is because you are adding a UILabel as a subview of a UIView, but you're not giving that UIView any constraints. So the view ends up with a width and height of Zero, and the label exists outside the bounds of the view.
Without seeing all of your code, it doesn't look like you need the extra view holding the label.
Take a look at this... it will add a vertical stack view to the main view - centered X and Y - and add "colorChoice" labels to the stack view:
class TestViewController: UIViewController {
let stack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 4
return v
}()
var colorLabelDict: [Int: UIView] = [:]
override func viewDidLoad() {
super.viewDidLoad()
let v1 = makeColorLabel(colorName: "red", bgColor: .red, fgColor: .white)
let v2 = makeColorLabel(colorName: "green", bgColor: .green, fgColor: .black)
let v3 = makeColorLabel(colorName: "blue", bgColor: .blue, fgColor: .white)
[v1, v2, v3].forEach {
stack.addArrangedSubview($0)
}
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
func makeColorLabel(colorName:String, bgColor:UIColor, fgColor:UIColor) -> UILabel {
let colorNumber:Int = colorLabelDict.count
// create tap gesture recognizer
let tapColorGR:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapColor))
let colorChoice: UILabel = {
let l = UILabel()
l.tag = 700 + colorNumber
l.addGestureRecognizer(tapColorGR)
l.text = colorName
l.textAlignment = .center
l.textColor = fgColor
l.backgroundColor = bgColor
l.font = UIFont.systemFont(ofSize: 24, weight: .bold)
l.layer.borderColor = fgColor.cgColor
l.layer.borderWidth = 1
l.layer.cornerRadius = 20
l.layer.masksToBounds = true
l.adjustsFontSizeToFitWidth = true
l.translatesAutoresizingMaskIntoConstraints = false
// default .isUserInteractionEnabled for UILabel is false, so enable it
l.isUserInteractionEnabled = true
return l
}()
NSLayoutConstraint.activate([
// label height: 50, width: 100
colorChoice.heightAnchor.constraint(equalToConstant: 50),
colorChoice.widthAnchor.constraint(equalToConstant: 100),
])
// assign reference to this label in colorLabelDict dictionary
colorLabelDict[colorNumber] = colorChoice
// return newly created label
return colorChoice
}
#objc func tapColor(sender:UITapGestureRecognizer) {
print("A Color was tapped...with tag:\(sender.view?.tag ?? -1)")
// unwrap the view that was tapped, make sure it's a UILabel
guard let tappedView = sender.view as? UILabel else {
return
}
let cn = tappedView.tag
let colorNumber = cn
print("The \(tappedView.text ?? "No text") label was tapped.")
}
}
Result of running that:
Those are 3 UILabels, and tapping each will trigger the tapColor() func, printing this to the debug console:
A Color was tapped...with tag:700
The red label was tapped.
A Color was tapped...with tag:701
The green label was tapped.
A Color was tapped...with tag:702
The blue label was tapped.

Related

Adding labels and textviews in a stack view programmatically in swift

How can I do to have a title, followed by a few lines of text, followed by a title again and again few lines of text constrained in the middle of a view controller programmatically?
My goal is to have bolded for the titles, and it would be nice to have the textview lines incremented also.
My idea was to create 2 labels, and 2 textviews. And adding those to a textview in this order: label1, t1, label2, t2.
But it doesn't seem to work. I try to avoid defining the same textviews and labels many times. textviews add up if I copy its definition twice but not for labels (maybe it is view related?)
I tried with UIbuttons and it worked.
This is what I tried so far:
import UIKit
class HowToSetupProIGVC: UIViewController {
deinit {print("deinit")}
let textView: UITextView = {
let textView = UITextView()
textView.backgroundColor = .blue //bkgdColor
textView.textAlignment = .left
//textView.frame = CGRect(x: 5, y: 5, width: 5, height: 5)
textView.tintColor = .black
textView.translatesAutoresizingMaskIntoConstraints = false //enable autolayout
textView.heightAnchor.constraint(equalToConstant: 100).isActive = true
textView.widthAnchor.constraint(equalToConstant: 300).isActive = true
return textView
}()
let label: UILabel = {
let l = UILabel(frame:CGRect.zero)
//l.frame = CGRect(x: 5, y: 5, width: 5, height: 5)
l.backgroundColor = .green //bkgdColor
l.font = UIFont.preferredFont(forTextStyle: .headline)
l.translatesAutoresizingMaskIntoConstraints = false //enable autolayout
l.heightAnchor.constraint(equalToConstant: 22).isActive = true
l.widthAnchor.constraint(equalToConstant: 300).isActive = true
return l
}()
override func viewDidLoad() {
super.viewDidLoad()
self.modalUI(arrowButton: false)
self.view.backgroundColor = bkgdColor
customStackHTSProIG ()
}
}
extension HowToSetupProIGVC {
func customStackHTSProIG () {
let label1 = label
let label2 = label
let t1 = textView
let t2 = textView
label1.text = "Title1:"
label2.text = "title2:"
t1.text = """
1. On your profile tap menu
2. Tap settings
3. Tap accounts
4. Tap set up professional account
"""
t2.text = """
1. On your profile tap "Edit profile"
2. Link your created page to your account
"""
//StackView
let stackHTS = UIStackView()
stackHTS.axis = NSLayoutConstraint.Axis.vertical
stackHTS.distribution = .fillEqually
stackHTS.alignment = .center
stackHTS.spacing = 5
stackHTS.backgroundColor = .red
//Add StackView + elements
stackHTS.addArrangedSubview(label1)
stackHTS.addArrangedSubview(t1)
stackHTS.addArrangedSubview(label2)
stackHTS.addArrangedSubview(t2)
self.view.addSubview(stackHTS)
//Constraints StackView
stackHTS.translatesAutoresizingMaskIntoConstraints = false
stackHTS.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
stackHTS.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
//stackHTS.heightAnchor.constraint(equalToConstant: 88).isActive = true
}
}
UILabel & UITextView are both UIKit classes written in Objective-C. They are reference types, NOT value types.
When you write following -
let label1 = label
let label2 = label
let t1 = textView
let t2 = textView
Both label1 & label2 are pointing to the one & same instance of UILabel. So is the case for t1 & t2 as well.
When you add them like this -
//Add StackView + elements
stackHTS.addArrangedSubview(label1)
stackHTS.addArrangedSubview(t1)
stackHTS.addArrangedSubview(label2)
stackHTS.addArrangedSubview(t2)
You expect 2 labels and 2 textViews to be added to the StackView. You are adding only 1 label and 1 textView though.
You expect to see all of following -
label1.text = "Title1:"
label2.text = "title2:"
t1.text = """
1. On your profile tap menu
2. Tap settings
3. Tap accounts
4. Tap set up professional account
"""
t2.text = """
1. On your profile tap "Edit profile"
2. Link your created page to your account
"""
However you are only seeing following -
label2.text = "title2:"
t2.text = """
1. On your profile tap "Edit profile"
2. Link your created page to your account
"""
Solutions -
Create two separate instances of UITextView & UILabel like you have already done for the first two and Add these new instances to stack view as well.
Use one UILabel and remove everything else. Use NSAttributedString API to stylize your text as you want for different sections / paragraphs and assign it to UILabel.attributedText.

Swift: button added within UIView not clickable

I have the following container view:
class NotificationsContainer: UIView {
init() {
super.init(frame: .zero)
controller.view.translatesAutoresizingMaskIntoConstraints = false
addSubview(controller.view)
controller.view.isHidden = true
self.isUserInteractionEnabled = true
self.clipsToBounds = false
configureAutoLayout()
}
var showNotifications = false {
didSet {
if showNotifications == true {
controller.view.isHidden = false
} else {
controller.view.isHidden = true
}
}
}
internal lazy var notificationBanner: AlertView = {
let banner = AlertView()
banner.attrString = UploadNotificationManager.shared.notificationBannerText()
banner.alertType = .notification
banner.translatesAutoresizingMaskIntoConstraints = false
addSubview(banner)
banner.isUserInteractionEnabled = true
banner.showMeButton.addTarget(self, action: #selector(showHideNotifications), for: .touchDown)
return banner
}()
#objc func showHideNotifications() {
showNotifications = showNotifications == false ? true : false
}
private lazy var notificationView: NotificationContentView = {
let notificationView = NotificationContentView()
return notificationView
}()
private lazy var controller: UIHostingController = {
return UIHostingController(rootView: notificationView)
}()
private func configureAutoLayout() {
NSLayoutConstraint.activate([
notificationBanner.leadingAnchor.constraint(equalTo: leadingAnchor),
notificationBanner.trailingAnchor.constraint(equalTo: trailingAnchor),
controller.view.trailingAnchor.constraint(equalTo: notificationBanner.trailingAnchor),
controller.view.topAnchor.constraint(equalTo: notificationBanner.bottomAnchor)
])
}
}
AlertView contains a button as follows:
internal lazy var showMeButton: UIButton = {
let button = UIButton()
button.setTitle("Show me...", for: .normal)
button.setTitleColor(UIColor.i6.blue, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: Constants.fontSize)
addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
Then I add the container view to my main view:
private lazy var notifications: NotificationsContainer = {
let notifications = NotificationsContainer()
notifications.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(notifications)
notifications.leadingAnchor.constraint(equalTo: flightNumber.leadingAnchor).isActive = true
notifications.trailingAnchor.constraint(equalTo: flightNumber.trailingAnchor).isActive = true
return notifications
}()
override public func viewDidLoad() {
super.viewDidLoad()
stackView.insert(arrangedSubview: notifications, atIndex: 0)
}
Now as you can see I am trying to add an action to the showMeButton. However, when I click on the button, it does nothing. I have read before that this could be to do with the frame of the container view. However, I have tried setting the height of the notification view in my main view (width should already be there due to leading and trailing constraints) and I have tried setting the height of notificationBanner as well but nothing is working.
Here is the view in the view debugger:
The showMe button does not appear to be obscured and all other views appear to have dimensions...
Look at the debug view hierarchy in Xcode and see if the view containing the button is actually showing up. You haven't set enough constraints on any of these views so the height and width look like they could be ambiguous to me. Once you're inside the view debugger, another common problem is that another invisible view is covering up the one with the button and intercepting the touch gestures.

Why does my function to adjust view according to keyboard height behave erratically?

I am trying to re-create a Facebook sign up page for practice; the set up of my viewController is as follows:
1)A profile image container at the top,
2)Email textfield
3)Password Textfield
4)Confirm Password textfield
To solve the issue of the keyboard blocking the "Confirm Password" field, I have used listeners as below.
The issue is that the very first time a user clicks on a textfield to type (no matter which one), the screen moves way too far up, such that the email textfield at the top ends up going beyond the screen size on top. However, when I dismiss the keyboard and re-click on any textfield, it goes up as intended: Only until the top textfield reaches the top of the screen.
I can find no other reason for this behaviour, it persisted even after I re-wrote some of my code.
My ViewController set up:
class ViewController: UIViewController {
#objc func dismissKeyboard() {
view.endEditing(true)
}
let imageContainer: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
return v
}()
let emailTF: UITextField = {
let tf = UITextField()
tf.translatesAutoresizingMaskIntoConstraints = false
tf.placeholder = "EMAIL"
return tf
}()
let passwordTF: UITextField = {
let tf = UITextField()
tf.translatesAutoresizingMaskIntoConstraints = false
tf.placeholder = "Password"
return tf
}()
let confirmPasswordTF: UITextField = {
let tf = UITextField()
tf.translatesAutoresizingMaskIntoConstraints = false
tf.placeholder = "Confirm password"
return tf
}()
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.origin.y == 0 {
self.view.frame.origin.y -= keyboardSize.height
}
}
}
#objc func keyboardWillHide(notification: NSNotification) {
if self.view.frame.origin.y != 0 {
self.view.frame.origin.y = 0
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .lightGray
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
view.addSubview(imageContainer)
view.addSubview(emailTF)
view.addSubview(passwordTF)
view.addSubview(confirmPasswordTF)
NSLayoutConstraint.activate([
imageContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
imageContainer.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.275),
imageContainer.widthAnchor.constraint(equalTo: view.heightAnchor),
imageContainer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
emailTF.topAnchor.constraint(equalTo: imageContainer.bottomAnchor),
emailTF.heightAnchor.constraint(equalToConstant: 100),
emailTF.widthAnchor.constraint(equalToConstant: 200),
emailTF.centerXAnchor.constraint(equalTo: view.centerXAnchor),
passwordTF.topAnchor.constraint(equalTo: emailTF.bottomAnchor, constant: 20),
passwordTF.heightAnchor.constraint(equalToConstant: 100),
passwordTF.widthAnchor.constraint(equalToConstant: 200),
passwordTF.centerXAnchor.constraint(equalTo: view.centerXAnchor),
confirmPasswordTF.topAnchor.constraint(equalTo: passwordTF.bottomAnchor, constant: 20),
confirmPasswordTF.heightAnchor.constraint(equalToConstant: 100),
confirmPasswordTF.widthAnchor.constraint(equalToConstant: 200),
confirmPasswordTF.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
let tapToDismissKeyboard: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.dismissKeyboard))
view.addGestureRecognizer(tapToDismissKeyboard)
}
}
I'd really appreciate some help as to what I can do to correct this issue.
Seems like this is a known issue raised here and other SO questions too..
Two points to make:-
1) Use UIResponder.keyboardFrameEndUserInfoKey instead of UIResponder.keyboardFrameBeginUserInfoKey at:
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
in keyboardWillShow()
2) Doing so, you will get the total height of the keyboard including the toolbar. Which means:
However, when I dismiss the keyboard and re-click on any textfield, it
goes up as intended: Only until the top textfield reaches the top of
the screen.
This is the buggy behaviour where you get an incorrect height excluding the toolbar and:
The issue is that the very first time a user clicks on a textfield to
type (no matter which one), the screen moves way too far up, such that
the email textfield at the top ends up going beyond the screen size on
top.
This is actually the intended behaviour with actual height. You could calculate the height of the toolbar and reduce it from this height to achieve your goal! If your VC is embedded in a navigation controller, you could use this
Hope this helps.

removeFromSubView disables interaction

I have a UICollectionView that is basically a chat log. I have an imageView in some of the cells and added the ability to expand an image to full screen on tap.
///
ChatLogMessageCell.swift
/**
*
* I add the target to the UIButton with an image as a background
*/
messageImage.addTarget(self, action: #selector(fullscreenImage), for: .touchUpInside)
/*
* Full screen code
*/
#objc func fullscreenImage() {
if let chatlog = parentViewController as? ChatLogController {
let imageScroll = UIScrollView()
imageScroll.delegate = self
imageScroll.minimumZoomScale = 1.0
imageScroll.maximumZoomScale = 5.0
imageScroll.frame = UIScreen.main.bounds
let newImageView = UIImageView(image: messageImage.backgroundImage(for: .normal))
newImageView.frame = UIScreen.main.bounds
newImageView.backgroundColor = .black
newImageView.contentMode = .scaleAspectFit
newImageView.isUserInteractionEnabled = true
imageScroll.addSubview(newImageView)
chatlog.view.addSubview(imageScroll)
chatlog.navigationController?.isNavigationBarHidden = true
chatlog.tabBarController?.tabBar.isHidden = true
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissFullscreenImage))
newImageView.addGestureRecognizer(tap)
}
}
#objc func dismissFullscreenImage(_ sender: UITapGestureRecognizer) {
if let chatlog = parentViewController as? ChatLogController {
chatlog.navigationController?.isNavigationBarHidden = false
chatlog.tabBarController?.tabBar.isHidden = false
sender.view?.removeFromSuperview()
}
}
When The fullscreen image is removed the ChatLogController is no longer interactable. I can't scroll or re-enter fullscreen mode on an image.What am I missing here? I simply want to dismiss the full screen image and allow the user to choose another image or just scroll through the messages.
Here you remove the imageView
sender.view?.removeFromSuperview()
while you need to remove the scrollView like
sender.view?.superview?.removeFromSuperview()

Is this a UITextView transparency bug?

This issue came up in relation to a problem I had yesterday for which I should be able to create a workaround. As I investigated further, I found that it occurs more broadly than I originally thought. I had previously only noticed it in displayed text that included at least one newline character, but that's not the case below.
The problem seems to result from using the NSLayoutManager's boundingRect method to obtain (among other things) individual character widths and then using those widths to set characters' UITextView frame width properties. Doing so apparently causes the setting of the text view's backgroundColor to UIColor.clear to be ignored (i.e., the background becomes opaque). The Playground code below reproduces the problem, shown in red text, and shows the workaround of using a constant for widths, in black. The tighter the kerning, the more pronounced the effect.
Is this a bug? Or is it a quirk due to something else?
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.bounds = CGRect(x: -100, y: -100, width: 200, height: 200)
view.backgroundColor = .white
let str = "..T.V.W.Y.."
let strStorage = NSTextStorage(string: str)
let layoutManager = NSLayoutManager()
strStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: view.bounds.size)
textContainer.lineFragmentPadding = 0.0
layoutManager.addTextContainer(textContainer)
let strArray = Array(str)
struct CharInfo {
var char: Character
var origin: CGPoint?
var size: CGSize?
}
var charInfoArray = [CharInfo]()
for index in 0..<str.count {
charInfoArray.append(CharInfo.init(char: strArray[index], origin: nil, size: nil))
let charRange = NSMakeRange(index, 1)
let charRect = layoutManager.boundingRect(forGlyphRange: charRange, in: textContainer)
charInfoArray[index].origin = charRect.origin
charInfoArray[index].size = charRect.size
}
for charInfo in charInfoArray {
let textView0 = UITextView()
textView0.backgroundColor = UIColor.clear // Ignored in this case!!
textView0.text = String(charInfo.char)
textView0.textContainerInset = UIEdgeInsets.zero
let size0 = charInfo.size!
textView0.frame = CGRect(origin: charInfo.origin!, size: size0)
textView0.textContainer.lineFragmentPadding = CGFloat(0.0)
textView0.textColor = UIColor.red
view.addSubview(textView0)
let textView1 = UITextView()
textView1.backgroundColor = UIColor.clear // Required
textView1.text = String(charInfo.char)
textView1.textContainerInset = UIEdgeInsets.zero
var size1 = charInfo.size!
size1.width = 20 // But changing .height has no effect on opacity
textView1.frame = CGRect(origin: charInfo.origin!, size: size1)
textView1.frame = textView1.frame.offsetBy(dx: 0, dy: 20)
textView1.textContainer.lineFragmentPadding = CGFloat(0.0)
textView1.textColor = UIColor.black
view.addSubview(textView1)
}
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
This does seem to be a bug, but it's with NSLayoutManager's instance method boundingRect(forGlyphRange:in:). It only looks like it could be a transparency change.
According to Apple's documentation, boundingRect(forGlyphRange:in:) is supposed to "[return] a single bounding rectangle (in container coordinates) enclosing all glyphs and other marks drawn in the given text container for the given glyph range, including glyphs that draw outside their line fragment rectangles and text attributes such as underlining." But that's not what it's doing.
In this case, the width of each boundingRect gets reduced by the amount that the next glyph was shifted to the left, due to kerning. You can test this, for example, using str = "ToT" and adding print(size0.width) right after it is set. You'll get this:
6.0 // "T"; should have been 7.330078125
6.673828125 // "o"
7.330078125 // "T"
Until this bug is fixed, a workaround would be to calculate glyph size for each character in isolation.