BobA
BobA

Reputation: 1

Non-Storyboard / Programmatically-Generated UIViewControllers

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

Answers (1)

DonMag
DonMag

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

Related Questions