Reputation: 1
I have been researching how to add some special effects (fireworks) to my app, and doing so appears to require the programmatic generation of UIViews and UIViewControllers, as well as no longer being able to use Storyboard-drawn segues in a few areas. After a lot of research, I have found a tremendous amount of conflicting information, leaving me with several issues, the first of which is noted below:
In my current app I have programmatically generated a scrollable UITextView containing UILabels and UIButtons (associated with @objc functions), all within a Storyboard-drawn UIViewController. My attempts to do the same within a programmatically-generated UIViewController (and delegate) have resulted in a UITextView that will not scroll at all.
I’m using Swift 5, UIKit, and Xcode 15.0.
Thanks in advance for any suggestions you may have as to why my UITextView will not scroll.
I have included the some of the code for a stripped-down hard-coded test version, including “ViewController”, “GameViewController”, “UIViewController+Extensions”, and “NSLayoutConstraint+Extensions”. I have made no changes to AppDelegate and SceneDelegate, so they are not included herein. The view associated with ViewController is drawn in the “Main” storyboard, and it simply includes a label at the top and two buttons, one setting a variable to generate fireworks, and the other to not generate them.
I create a UIScrollView and a UIViewController in ViewController as follows:
class ViewController: UIViewController {
let scrollView :UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.isScrollEnabled = true
scrollView.isPagingEnabled = true
return scrollView
}()
@IBAction func aYesShowFireworks(_ sender: Any) {
aCreateView()
aCreateController()
}
func aCreateView() {
view = UIView(frame: UIScreen.main.bounds)
scrollView.frame = view.frame
view.addSubview(scrollView)
NSLayoutConstraint.pin(view: scrollView, to: view)
}
func aCreateController() {
GameViewController().view.frame = view.frame
GameViewController().view.frame.origin.x = scrollView.contentSize.width
scrollView.contentSize.width += view.frame.size.width
add(child: GameViewController(), in: scrollView)
}
}
I programmatically generate a UITextView in GameViewController as follows:
class GameViewController : UIViewController, UITextViewDelegate {
let vc: ViewController = ViewController()
let screenLabel = UILabel()
let gameStatusTextview = UITextView(frame: .zero)
var scount = 0
var xPosition = 7
var yPosition = 0
func aRefreshView () {
let subViews = self.gameStatusTextview.subviews
for subview in subViews {
subview.removeFromSuperview()
}
screenLabel.frame = CGRect(x: 50, y: 50, width: 300, height: 50)
screenLabel.text = "Fireworks Experiment 03"
screenLabel.textColor = appColorBlack
screenLabel.backgroundColor = appColorLightBlue
screenLabel.textAlignment = NSTextAlignment.center
screenLabel.font = UIFont.systemFont(ofSize: 25, weight: UIFont.Weight.regular)
view.addSubview(screenLabel)
gameStatusTextview.frame = CGRect(x: 5, y: 100, width: 380, height: 412)
gameStatusTextview.translatesAutoresizingMaskIntoConstraints = false
gameStatusTextview.delegate = self
gameStatusTextview.isEditable = false
gameStatusTextview.isUserInteractionEnabled = true
gameStatusTextview.isScrollEnabled = true
gameStatusTextview.backgroundColor = appColorMediumGreen
gameStatusTextview.contentInsetAdjustmentBehavior = .automatic
view.addSubview(gameStatusTextview)
while scount < 5 {
configureLabel(...)
gameStatusTextview.addSubview(each label)
configureButton(...)
gameStatusTextview.addSubview(each button)
yPosition += 58
xPosition = 7
scount += 1
}
xPosition = 175
yPosition = 650
configureButtonMain(...)
view.addSubview(return button)
}
func configureLabel (labelNameIn: UILabel, textIn: String, textColorIn: UIColor,
backgroundColorIn: UIColor, xPositionIn: Int, yPositionIn: Int,
widthIn: Int, heightIn: Int, boldOrRegularIn: String, fontSizeIn: CGFloat) {
labelNameIn.frame = CGRect(x: xPositionIn, y: yPositionIn, width: widthIn,
height: heightIn)
labelNameIn.text = textIn
labelNameIn.textColor = textColorIn
labelNameIn.backgroundColor = backgroundColorIn
labelNameIn.textAlignment = NSTextAlignment.left
labelNameIn.font = UIFont.systemFont(ofSize: fontSizeIn, weight: UIFont.Weight.regular)
return
}
func configureButton (buttonNameIn: UIButton, xPositionIn: Int, yPositionIn: Int,
textIn: String, textColorIn: UIColor, backgroundColorIn: UIColor,
selectorIdx: Int) {
var buttonFontSize: CGFloat = 0
buttonFontSize = 15
buttonNameIn.frame = CGRect(x: xPositionIn, y: yPositionIn, width: 0, height: 0)
buttonNameIn.setTitle(textIn, for: UIControl.State.normal)
buttonNameIn.titleLabel?.numberOfLines = 2
buttonNameIn.titleLabel?.lineBreakMode = .byWordWrapping
buttonNameIn.titleLabel?.textAlignment = .center
buttonNameIn.setTitleColor(textColorIn, for: .normal)
buttonNameIn.backgroundColor = backgroundColorIn
buttonNameIn.titleLabel?.font = UIFont.systemFont(ofSize: buttonFontSize, weight: UIFont.Weight.regular)
buttonNameIn.sizeToFit()
buttonNameIn.addTarget(self,
action: selectorNames[selectorIdx],
for: .touchUpInside)
return
}
func configureButtonMain (buttonNameIn: UIButton, xPositionIn: Int, yPositionIn: Int,
textIn: String, textColorIn: UIColor, backgroundColorIn: UIColor,
selectorIdx: Int) {
var buttonFontSize: CGFloat = 0
buttonFontSize = 15
buttonNameIn.frame = CGRect(x: xPositionIn, y: yPositionIn, width: 0, height: 0)
buttonNameIn.setTitle(textIn, for: UIControl.State.normal)
buttonNameIn.titleLabel?.numberOfLines = 2
buttonNameIn.titleLabel?.lineBreakMode = .byWordWrapping
buttonNameIn.titleLabel?.textAlignment = .center
buttonNameIn.setTitleColor(textColorIn, for: .normal)
buttonNameIn.backgroundColor = backgroundColorIn
buttonNameIn.titleLabel?.font = UIFont.systemFont(ofSize: buttonFontSize, weight: UIFont.Weight.regular)
buttonNameIn.sizeToFit()
buttonNameIn.addTarget(self,
action: selectorNames[selectorIdx],
for: .touchUpInside)
return
}
override func viewDidLoad() {
super.viewDidLoad()
aRefreshView()
}
}
Here is UIViewController+Extensions.swift
import UIKit.UIViewController
extension UIViewController {
remove child controller */
func add(child childViewController: UIViewController) {
beginAddChild(child: childViewController)
view.addSubview(childViewController.view)
endAddChild(child: childViewController)
}
add child controller in a specific view */
func add(child childViewController: UIViewController, in view: UIView) {
beginAddChild(child: childViewController)
view.addSubview(childViewController.view)
endAddChild(child: childViewController)
}
add child controller in a specific view with a set frame */
func add(child childViewController: UIViewController, in view: UIView, with frame:CGRect) {
beginAddChild(child: childViewController)
childViewController.view.frame = frame
view.addSubview(childViewController.view)
endAddChild(child: childViewController)
}
remove child controller */
func remove(child childViewController:UIViewController){
childViewController.beginAppearanceTransition(false, animated: false)
childViewController.willMove(toParent: nil)
childViewController.view.removeFromSuperview()
childViewController.removeFromParent()
childViewController.endAppearanceTransition()
}
extract these common methods out to avoid code duplication */
private func beginAddChild(child childViewController:UIViewController){
childViewController.beginAppearanceTransition(true, animated: false)
self.addChild(childViewController)
}
private func endAddChild(child childViewController:UIViewController){
childViewController.didMove(toParent: self)
childViewController.endAppearanceTransition()
}
}
Here is NSLayoutConstraint+Extensions.swift
import UIKit
extension NSLayoutConstraint {
class func pin(view:UIView, to superview:UIView) {
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: superview.topAnchor),
view.trailingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.trailingAnchor),
view.leadingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.leadingAnchor),
view.bottomAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.bottomAnchor)
])
}
}
Upvotes: 0
Views: 126
Reputation: 77638
I have no idea what you might have done with your original Storyboard approach... a UITextView
will not scroll simply by adding subviews to it.
You can "fix" your scrolling issue with TWO edits, and ONE new line in GameViewController
...
Use a scroll view instead of a text view:
//let gameStatusTextview = UITextView()
let gameStatusTextview = UIScrollView()
Xcode will show you an error, so jump to it and comment-out (or delete) this line:
//gameStatusTextview.isEditable = false
Then, find your print("\(self) \(#function) player loop ended")
line of code, and add this above it:
// gameStatusTextview is now a UIScrollView, so
// set the .contentSize to make it scrollable
// we use 0 as the width, because we only want vertical scrolling
gameStatusTextview.contentSize = .init(width: 0, height: yPosition)
print("\(self) \(#function) player loop ended")
Now, you're using a UIScrollView
instead of a UITextView
, and your "player rows" will scroll vertically.
You also commented that "The viewDidLoad runs 3 times per screen display" ...
In aCreateController()
in your ViewController
:
func aCreateController() {
print("\(self) \(#function) about to add child GameViewController in scrollView")
print(" - GameViewController=\(GameViewController())")
GameViewController().view.frame = view.frame
GameViewController().view.frame.origin.x = scrollView.contentSize.width
scrollView.contentSize.width += view.frame.size.width
add(child: GameViewController(), in: scrollView)
print("\(self) \(#function) added child GameViewController in scrollView")
print(" - GameViewController=\(GameViewController())")
}
Every place you have this: GameViewController()
you are creating a new instance of that controller (and then immediately discarding it).
Change that func to:
func aCreateController() {
print("\(self) \(#function) about to add child GameViewController in scrollView")
let vc = GameViewController()
add(child: vc, in: scrollView)
vc.view.frame = view.frame
vc.view.frame.origin.x = scrollView.contentSize.width
scrollView.contentSize.width += view.frame.size.width
print("\(self) \(#function) added child GameViewController in scrollView")
}
As far as adding "Fireworks" -- that is a completely different topic. If you need help with that, post it as a new question.
Upvotes: 1