4
\$\begingroup\$

I made a custom UITextView for vertical Mongolian writing. It is made by subclassing UIView, which has a subview called rotatedView. This rotatedView itself has a UITextView subview. So there are three views: a parent, a child, and a grandchild.

enter image description here

The UITextView contains the actual text. The rotatedView is a container that gives the text the correct vertical orientation and line wrapping. The parent UIView is the container that allows autolayout to work normally in the Interface Builder. For more background, please see here and here.

Whenever the parent UIView frame changes, the old rotatedView is removed and a new instance is made. Then the UITextView (a property of the custom UIView subclass) is added to the rotatedView.

What I am worried about is memory leaks. Since the grandchild view UITextView is a property of my custom view but rotatedView isn't. When I drop rotatedView on a frame size change, is any reference kept to it since the UITextView property is a subview of rotatedView?

This demo project can be found on github, but here is the custom class code:

import UIKit

@IBDesignable class UIMongolTextView: UIView {

    // ********* Unique to TextView *********
    private let view = UITextView()
    
    let mongolFontName = "ChimeeWhiteMirrored"
    
    @IBInspectable var text: String {
        get {
            if let txt = view.text {
                return txt
            } else {
                return ""
            }
        }
        set {
            view.text = newValue
        }
    }
    
    func setup() {
        view.text = self.text
        view.backgroundColor = self.backgroundColor
        view.font = UIFont(name: mongolFontName, size: 24)
    }
    
    
    
    
    // *******************************************
    // ****** General code for Mongol views ******
    // *******************************************
    
    private var oldWidth: CGFloat = 0
    private var oldHeight: CGFloat = 0
    
    // This method gets called if you create the view in the Interface Builder
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    // This method gets called if you create the view in code
    override init(frame: CGRect){
        super.init(frame: frame)
        self.setup()
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        self.setup()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // layoutSubviews gets called multiple times, only need it once
        if self.frame.height == oldHeight && self.frame.width == oldWidth {
            return
        } else {
            oldWidth = self.frame.width
            oldHeight = self.frame.height
        }
        
        // Remove the old rotation view
        if self.subviews.count > 0 {
            self.subviews[0].removeFromSuperview()
        }
        
        // setup rotationView container
        let rotationView = UIView()
        rotationView.frame = CGRect(origin: CGPointZero, size: CGSize(width: self.bounds.height, height: self.bounds.width))
        self.addSubview(rotationView)
        
        // transform rotationView (so that it covers the same frame as self)
        rotationView.transform = translateRotateFlip()
        
        
        
        // add view
        view.frame = rotationView.bounds
        rotationView.addSubview(view)
        
    }
    
    func translateRotateFlip() -> CGAffineTransform {
        
        var transform = CGAffineTransformIdentity
        
        // translate to new center
        transform = CGAffineTransformTranslate(transform, (self.bounds.width / 2)-(self.bounds.height / 2), (self.bounds.height / 2)-(self.bounds.width / 2))
        // rotate counterclockwise around center
        transform = CGAffineTransformRotate(transform, CGFloat(-M_PI_2))
        // flip vertically
        transform = CGAffineTransformScale(transform, CGFloat(-1), CGFloat(1))
        
        return transform
    }

}

I am using the same general method for text views, labels, and table views.

Update

The advice below is extremely useful. The things I've already tried to apply can be seen in the code of this question.

However, no one has yet commented on my original reason for posting this question. That is, is there any sort of memory leak issue with removing and then recreating a subview that itself has a property subview? I'm going to take that to mean that there isn't a big problem with this, so I won't worry about it.

\$\endgroup\$

2 Answers 2

4
\$\begingroup\$
@IBInspectable var text: String {
    get {
        if let txt = view.text {
            return txt
        } else {
            return ""
        }
    }
    set {
        view.text = newValue
    }
}

Just a small thing to comment on, however, a couple of notes here.

First, with Xcode 7, which is right around the corner, UITextView's text property will be a regular String instead of String!, so we can eliminate this if statement altogether.

However, in the meantime, this becomes a lot cleaner with the nil-coalescing operator:

get {
    return view.text ?? ""
}

Another minor comment:

transform = CGAffineTransformScale(transform, CGFloat(-1), CGFloat(1))

Using the CGFloat constructor here is unnecessary. We can just use the literals:

transform = CGAffineTransformScale(transform, -1, 1)
\$\endgroup\$
3
  • \$\begingroup\$ As an interesting side note, I also applied this to my custom Label. However, I had to use return view.text ?? "" rather than return view.text even though I am using Xcode 7 beta. It works for TextView but not for Label. Maybe this is just an issue with the beta version. \$\endgroup\$
    – Suragch
    Commented Aug 9, 2015 at 23:13
  • \$\begingroup\$ The nil-coalescing operator will only work with optional types. It's unnecessary with non-optionals because they can't be nil. And to be clear, the nil-coalescing operator has always existing in Swift. What's changing between versions is the return types of some of the UIKit properties. \$\endgroup\$
    – nhgrif
    Commented Aug 9, 2015 at 23:15
  • \$\begingroup\$ I thought it was interesting that label.text is an optional while textView.text isn't. See this question \$\endgroup\$
    – Suragch
    Commented Aug 9, 2015 at 23:45
4
\$\begingroup\$
func setup() {
    view.text = self.text
    view.backgroundColor = self.backgroundColor
    view.font = UIFont(name: mongolFontName, size: 24)
}

So, we've got this code in here, but it looks like the method is only called from the initializers and from awakeFromNib. So what happens if the background color is programmatically set after this point?

But there's another problem... we're stuck using a specific font and a specific size. There's no way to set the font to anything else.

Also, setting the view.text here doesn't even make sense. Remember how the text property we created works? It just returns view.text ?? "". So in effect, this line is just a shortcut for:

view.text = view.text ?? ""

And ultimately, setting the view's background seems wholly unnecessary here. Why don't we just give all of the view's a transparent background and let self.backgroundColor show through?

Finally, methods should be verbs. They describe an action. But setup is actually a noun (properties should be nouns). We can fix this by capitalizing the u, making the method name setUp--a verb.

So, setUp should do our one-time, first time initialization. What do we need to do here?

  • Set all of our subviews to have a clear background color.

That's all we really need to do at this point.

The text property we created takes care of the text field's text.

We should similarly create a font property. Or perhaps other fonts don't work this text view. We can still make a fontSize property to allow the user to set the size they want. Nicely, fontSize could be an IBInspectable too (UIFont unfortunately cannot).

So, either we want an entire font property exposed to allow the user to pick font:

var font: UIFont = UIFont(name: mongolFontName, size: 24) {
    get {
        return view.font
    }
    set {
        view.font = newValue
    }
}

If we make all of the subviews have clear backgrounds, we don't have to worry about doing anything to handle this. We can just rely on the default behavior of setting a view's background color, which we're inheriting from UIView already.


You also need to keep in mind that we're missing out on a lot of the functionality of the default UITextField with your class because you haven't provided a way to access all of the functionality.

There are at least two massive things I can think of off the top of my head:

  • placeholder
  • delegation

We can't set any placeholder text for your textfield.

We can't delegate the textfield at all.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.