How to resize a NSTextView automatically as per its content? - swift

I am making an app where a user can click anywhere on the window and a NSTextView is added programmatically at the mouse location. I have got it working with the below code but I want this NSTextView to horizontally expand until it reaches the edge of the screen and then grow vertically. It currently has a fixed width and when I add more characters, the text view grows vertically (as expected) but I also want it to grow horizontally. How can I achieve this?
I have tried setting isHorizontallyResizable and isVerticallyResizable to true but this doesn't work. After researching for a while, I came across this https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextStorageLayer/Tasks/TrackingSize.html but this didn't work for me either.
Code in my ViewController to add the NSTextView to its view:
private func addText(at point: NSPoint) {
let textView = MyTextView(frame: NSRect(origin: point, size: CGSize(width: 150.0, height: 40.0)))
view.addSubview(textView)
}
And, MyTextView class looks like below:
class MyTextView: NSTextView {
override func viewWillDraw() {
isHorizontallyResizable = true
isVerticallyResizable = true
isRichText = false
}
}
I have also seen this answer https://stackoverflow.com/a/54228147/1385441 but I am not fully sure how to implement it. I have added this code snippet in MyTextView and used it like:
override func didChangeText() {
frame.size = contentSize
}
However, I think I am using it incorrectly. Ergo, any help would be much appreciated.

I'm a bit puzzled, because you're adding NSTextView to a NSView which is part of the NSViewController and then you're talking about the screen width. Is this part of your Presentify - Screen Annotation application? If yes, you have a full screen overlay window and you can get the size from it (or from the view controller's view).
view.bounds.size // view controller's view size
view.window?.frame.size // window size
If not and you really need to know the screen size, check the NSWindow & NSScreen.
view.window?.screen?.frame.size // screen size
Growing NSTextView
There's no any window/view controller's view resizing behavior specified.
import Cocoa
class BorderedTextView: NSTextView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let path = NSBezierPath(rect: bounds)
NSColor.red.setStroke()
path.stroke()
}
}
class ViewController: NSViewController {
override func mouseUp(with event: NSEvent) {
// Convert point to the view coordinates
let point = view.convert(event.locationInWindow, from: nil)
// Initial size
let size = CGSize(width: 100, height: 25)
// Maximum text view width
let maxWidth = view.bounds.size.width - point.x // <----
let textView = BorderedTextView(frame: NSRect(origin: point, size: size))
textView.insertionPointColor = .orange
textView.drawsBackground = false
textView.textColor = .white
textView.isRichText = false
textView.allowsUndo = false
textView.font = NSFont.systemFont(ofSize: 20.0)
textView.isVerticallyResizable = true
textView.isHorizontallyResizable = true
textView.textContainer?.widthTracksTextView = false
textView.textContainer?.heightTracksTextView = false
textView.textContainer?.size.width = maxWidth // <----
textView.maxSize = NSSize(width: maxWidth, height: 10000) // <----
view.addSubview(textView)
view.window?.makeFirstResponder(textView)
}
}

I finally got it to work (except for one minor thing). The link from Apple was the key here but they haven't described the code completely, unfortunately.
The below code work for me:
class MyTextView: NSTextView {
override func viewWillDraw() {
// for making the text view expand horizontally
textContainer?.heightTracksTextView = false
textContainer?.widthTracksTextView = false
textContainer?.size.width = 10000.0
maxSize = NSSize(width: 10000.0, height: 10000.0)
isHorizontallyResizable = true
isVerticallyResizable = true
isRichText = false
}
}
That one minor thing which I haven't been able to figure out yet is to limit expanding horizontally until the edge of the screen is reached. Right now it keeps on expanding even beyond the screen width and, in turn, the text is hidden after the screen width.
I think if I can somehow get the screen window width then I can replace 10000.0 with the screen width (minus the distance of text view from left edge) and I can limit the horizontal expansion until the edge of the screen. Having said that, keeping it 10000.0 won't impact performance as described in the Apple docs.

Related

Identifying Objects in Firebase PreBuilt UI in Swift

FirebaseUI has a nice pre-buit UI for Swift. I'm trying to position an image view above the login buttons on the bottom. In the example below, the imageView is the "Hackathon" logo. Any logo should be able to show in this, if it's called "logo", since this shows the image as aspectFit.
According to the Firebase docs page:
https://firebase.google.com/docs/auth/ios/firebaseui
You can customize the signin screen with this function:
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
return FUICustomAuthPickerViewController(nibName: "FUICustomAuthPickerViewController",
bundle: Bundle.main,
authUI: authUI)
}
Using this code & poking around with subviews in the debuggers, I've been able to identify and color code views in the image below. Unfortunately, I don't think that the "true" size of these subview frames is set until the view controller presents, so trying to access the frame size inside these functions won't give me dimensions that I can use for creating a new imageView to hold a log. Plus accessing the views with hard-coded index values like I've done below, seems like a pretty bad idea, esp. given that Google has already changed the Pre-Built UI once, adding a scroll view & breaking the code of anyone who set the pre-built UI's background color.
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
// Create an instance of the FirebaseAuth login view controller
let loginViewController = FUIAuthPickerViewController(authUI: authUI)
// Set background color to white
loginViewController.view.backgroundColor = UIColor.white
loginViewController.view.subviews[0].backgroundColor = UIColor.blue
loginViewController.view.subviews[0].subviews[0].backgroundColor = UIColor.red
loginViewController.view.subviews[0].subviews[0].tag = 999
return loginViewController
}
I did get this to work by adding a tag (999), then in the completion handler when presenting the loginViewController I hunt down tag 999 and call a function to add an imageView with a logo:
present(loginViewController, animated: true) {
if let foundView = loginViewController.view.viewWithTag(999) {
let height = foundView.frame.height
print("FOUND HEIGHT: \(height)")
self.addLogo(loginViewController: loginViewController, height: height)
}
}
func addLogo(loginViewController: UINavigationController, height: CGFloat) {
let logoFrame = CGRect(x: 0 + logoInsets, y: self.view.safeAreaInsets.top + logoInsets, width: loginViewController.view.frame.width - (logoInsets * 2), height: self.view.frame.height - height - (logoInsets * 2))
// Create the UIImageView using the frame created above & add the "logo" image
let logoImageView = UIImageView(frame: logoFrame)
logoImageView.image = UIImage(named: "logo")
logoImageView.contentMode = .scaleAspectFit // Set imageView to Aspect Fit
// loginViewController.view.addSubview(logoImageView) // Add ImageView to the login controller's main view
loginViewController.view.addSubview(logoImageView)
}
But again, this doesn't seem safe. Is there a "safe" way to deconstruct this UI to identify the size of this button box at the bottom of the view controller (this size will vary if there are multiple login methods supported, such as Facebook, Apple, E-mail)? If I can do that in a way that avoids the hard-coding approach, above, then I think I can reliably use the dimensions of this button box to determine how much space is left in the rest of the view controller when adding an appropriately sized ImageView. Thanks!
John
This should address the issue - allowing a logo to be reliably placed above the prebuilt UI login buttons buttons + avoiding hard-coding the index values or subview locations. It should also allow for properly setting background color (also complicated when Firebase added the scroll view + login button subview).
To use: Create a subclass of FUIAuthDelegate to hold a custom view controller for the prebuilt Firebase UI.
The code will show the logo at full screen behind the buttons if there isn't a scroll view or if the class's private constant fullScreenLogo is set to false.
If both of these conditions aren't meant, the logo will show inset taking into account the class's private logoInsets constant and the safeAreaInsets. The scrollView views are set to clear so that a background image can be set, as well via the private let backgroundColor.
Call it in any signIn function you might have, after setting authUI.providers. Call would be something like this:
let loginViewController = CustomLoginScreen(authUI: authUI!)
let loginNavigationController = UINavigationController(rootViewController: loginViewController)
loginNavigationController.modalPresentationStyle = .fullScreen
present(loginNavigationController, animated: true, completion: nil)
And here's one version of the subclass:
class CustomLoginScreen: FUIAuthPickerViewController {
private var fullScreenLogo = false // false if you want logo just above login buttons
private var viewContainsButton = false
private var buttonViewHeight: CGFloat = 0.0
private let logoInsets: CGFloat = 16
private let backgroundColor = UIColor.white
private var scrollView: UIScrollView?
private var viewContainingButton: UIView?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// set color of scrollView and Button view inside scrollView to clear in viewWillAppear to avoid a "color flash" when the pre-built login UI first appears
self.view.backgroundColor = UIColor.white
guard let foundScrollView = returnScrollView() else {
print("😡 Couldn't get a scrollView.")
return
}
scrollView = foundScrollView
scrollView!.backgroundColor = UIColor.clear
guard let foundViewContainingButton = returnButtonView() else {
print("😡 No views in the scrollView contain buttons.")
return
}
viewContainingButton = foundViewContainingButton
viewContainingButton!.backgroundColor = UIColor.clear
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Create the UIImageView at full screen, considering logoInsets + safeAreaInsets
let x = logoInsets
let y = view.safeAreaInsets.top + logoInsets
let width = view.frame.width - (logoInsets * 2)
let height = view.frame.height - (view.safeAreaInsets.top + view.safeAreaInsets.bottom + (logoInsets * 2))
var frame = CGRect(x: x, y: y, width: width, height: height)
let logoImageView = UIImageView(frame: frame)
logoImageView.image = UIImage(named: "logo")
logoImageView.contentMode = .scaleAspectFit // Set imageView to Aspect Fit
logoImageView.alpha = 0.0
// Only proceed with customizing the pre-built UI if you found a scrollView or you don't want a full-screen logo.
guard scrollView != nil && !fullScreenLogo else {
print("No scrollView found.")
UIView.animate(withDuration: 0.25, animations: {logoImageView.alpha = 1.0})
self.view.addSubview(logoImageView)
self.view.sendSubviewToBack(logoImageView) // otherwise logo is on top of buttons
return
}
// update the logoImageView's frame height to subtract the height of the subview containing buttons. This way the buttons won't be on top of the logoImageView
frame = CGRect(x: x, y: y, width: width, height: height - (viewContainingButton?.frame.height ?? 0.0))
logoImageView.frame = frame
self.view.addSubview(logoImageView)
UIView.animate(withDuration: 0.25, animations: {logoImageView.alpha = 1.0})
}
private func returnScrollView() -> UIScrollView? {
var scrollViewToReturn: UIScrollView?
if self.view.subviews.count > 0 {
for subview in self.view.subviews {
if subview is UIScrollView {
scrollViewToReturn = subview as? UIScrollView
}
}
}
return scrollViewToReturn
}
private func returnButtonView() -> UIView? {
var viewContainingButton: UIView?
for view in scrollView!.subviews {
viewHasButton(view)
if viewContainsButton {
viewContainingButton = view
break
}
}
return viewContainingButton
}
private func viewHasButton(_ view: UIView) {
if view is UIButton {
viewContainsButton = true
} else if view.subviews.count > 0 {
view.subviews.forEach({viewHasButton($0)})
}
}
}
Hope this helps any who have been frustrated trying to configure the Firebase pre-built UI in Swift.

Swift 5: centerXAnchor won't center my frame in the view

I'm stuck at the Consolidation IV challenge from hackingwithswift.com.
Right now I'm trying to create a hangman game. I thought to place placeholder labels based on the length of the answer word. These placeholder labels would be placed inside a frame, which then would be placed in the center of the main view.
Unfortunately, the leading edge of the frame is placed centered. In my opinion, this is not a problem of constraints, but rather a problem of me creating the frame wrong.
My current code is
import UIKit
class ViewController: UIViewController {
var answer: String!
override func viewDidLoad() {
super.viewDidLoad()
// MARK: - declare all the labels here
let letterView = UIView()
letterView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(letterView)
// MARK: - set constraints to all labels, buttons etc.
NSLayoutConstraint.activate([
letterView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
letterView.centerXAnchor.constraint(equalTo: view.layoutMarginsGuide.centerXAnchor)
])
// MARK: - populate the letterView
// set the size of the placeholder
answer = "Atmosphäre"
let height = 60
let width = 25
// var width: Int
for placeholder in 0..<answer.count {
// create new label and give it a big font size
let placeholderLabel = UILabel()
placeholderLabel.text = "_"
placeholderLabel.font = UIFont.systemFont(ofSize: 36)
// calculate frame of this label
let frame = CGRect(x: placeholder * width, y: height, width: width, height: height)
placeholderLabel.frame = frame
// add label to the label view
letterView.addSubview(placeholderLabel)
}
}
}
The simulator screen looks just like this:
I already searched for answers on stackoverflow, but wasn't successful. I think I don't know what I'm exactly looking for.
The main problem, is that the letterView has no size, because no width or height constraints are applied to it.
To fix your code make the letterView big enough to contain the labels you've added as subviews by adding height and width constraints after the for loop:
for placeholder in 0..<answer.count {
...
}
NSLayoutConstraint.activate([
letterView.widthAnchor.constraint(equalToConstant: CGFloat(width * answer.count)),
letterView.heightAnchor.constraint(equalToConstant: CGFloat(height))
])
I'm not sure if you've covered this in your course yet, but a better way to go about this (which would take much less code), is to use a UIStackView as your letterView instead.
An extra thing to consider:
If you give the letterView a background color, you'll see that the labels are actually aligned outside of its bounds:
That's because you're setting each label's y position to be height, when it should probably be zero:
let frame = CGRect(x: placeholder * width, y: 0, width: width, height: height)
Correcting this places the labels within the bounds of the letterView:

How to fix the wrong frame after device rotation?

I use custom alert view in my application. When called, its frame completely fills the screen. However, after the device rotates, it does not look right.
Then I try to change my alert frame and assign it a new screen size in viewWillTransition
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: nil) { _ in UIView.setAnimationsEnabled(true) }
UIView.setAnimationsEnabled(false)
super.viewWillTransition(to: size, with: coordinator)
if let _ = alert {
alert?.rotateAlert(to: size)
Logger.Log("after size = \(alert?.backgroundView.frame.size)")
Logger.Log("after frame = \(alert?.frame)")
Logger.Log("after bounds = \(alert?.bounds)")
}
I tried to do the same in viewDidLayoutSubviews
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let _ = alert {
Logger.Log("viewDidLayoutSubviews before rotation")
alert?.rotateAlert(to: self.view.frame.size)
Logger.Log("after size = \(alert?.backgroundView.frame.size)")
Logger.Log("after frame = \(alert?.frame)")
Logger.Log("after bounds = \(alert?.bounds)")
}
}
func rotateAlert(to size: CGSize) {
self.frame.size = size
backgroundView.frame = frame
}
Thus the frame of my alert changes. But its size is still smaller than the screen in height, thus forming an empty area.
Why is this happening? And how do I fix this? (I use Swift4)
To place your alert in the center of the screen, assuming that it is an instance of UIView, do this:
alert.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
alert.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
// Or
NSLayoutConstraint.activate([
alert.centerXAnchor.constraint(equalTo: view.centerXAnchor),
alert.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
This is using auto layout to position your views, and the views will properly update when the trait collection changes.
When adding a view programmatically the view doesn't adjust according to the screen rotation because you have fixed the view height, width so after rotation you need to again give its frame.
This doesn't happen when you use constraints to add a view either programmatically or from the storyboard
For your alert to resize you have to again give its frame in viewDidLayoutSubviews()
override func viewDidLayoutSubviews() {
YourAlertView.frame = CGRect(x:0 , y: 0,width: self.view.frame.width, height: self.view.frame.height )
}
Works like a charm for me.

How to set the window size and position programmatically for a SpriteKit/GameScene app on OSX

I have a bare-bones project created in Xcode as a SpriteKit/GameScene app. I want to set the window size programmatically. I've read a lot of answers here, and several tutorials elsewhere, but none of the things I've read have helped.
This answer talks about overriding WindowController.windowDidLoad, but GameScene doesn't give me a WindowController. It does give me a ViewController. This tutorial says you can call self.view.window?.setFrame() from ViewController.viewDidLoad(), but my window stubbornly remains the same size. A number of the answers I've found on SO talk about auto-layout. I don't have anything to lay out. It's just a SpriteKit starter project.
This answer says you can set preferredContentSize in ViewController.viewWillAppear(), and that does in fact let me set the window size, but if I make the window too big (because I had to guess at a legitimate size), it's not draggable or resizable. This answer says you can get the correct size from view.bounds.size, but that says 800 x 600, which is nowhere near the size of my screen. This answer says you can get the bounds from UIScreen.mainScreen().bounds.size, but my GameScene/SpriteKit starter project seems not to have any kind of UIScreen in it.
How can I get the screen size, and how can I set the window size and position programmatically?
Also, one of the other answers, which I can't seem to find any more, says you should delete GameScene.sks, which I did, and everything seems fine still, except for the size.
Updated for Swift 5
Window Size
Remember to add NSWindowDelegate to your NSViewController class if you wish to implement it there (ie. viewDidLoad(), viewWillAppear(), viewDidAppear(), etc.)
NSView
class ViewController: NSViewController, NSWindowDelegate {
override func viewWillAppear() {
fillWindow()
}
/// Sizes window to fill max screen size
func fillWindow() {
if let screenSize = view.window?.screen?.frame {
view.window!.setFrame(screenSize, display: true)
}
}
}
NSWindow
class WindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
fillWindow()
}
/// Sizes window to fill max screen size
func fillWindow() {
if let screenSize = window?.screen?.frame {
window!.setFrame(screenSize, display: true)
}
}
}
Print to Debugger Console
print(screenSize) // embed within if let screenSize { ... }
Window Position
See the full answer, with implemented code, here.
extension NSWindow {
/// Positions the `NSWindow` at the horizontal-vertical center of the `visibleFrame` (takes Status Bar and Dock sizes into account)
public func positionCenter() {
if let screenSize = screen?.visibleFrame.size {
self.setFrameOrigin(NSPoint(x: (screenSize.width-frame.size.width)/2, y: (screenSize.height-frame.size.height)/2))
}
}
/// Centers the window within the `visibleFrame`, and sizes it with the width-by-height dimensions provided.
public func setCenterFrame(width: Int, height: Int) {
if let screenSize = screen?.visibleFrame.size {
let x = (screenSize.width-frame.size.width)/2
let y = (screenSize.height-frame.size.height)/2
self.setFrame(NSRect(x: x, y: y, width: CGFloat(width), height: CGFloat(height)), display: true)
}
}
/// Returns the center x-point of the `screen.visibleFrame` (the frame between the Status Bar and Dock).
/// Falls back on `screen.frame` when `.visibleFrame` is unavailable (includes Status Bar and Dock).
public func xCenter() -> CGFloat {
if let screenSize = screen?.visibleFrame.size { return (screenSize.width-frame.size.width)/2 }
if let screenSize = screen?.frame.size { return (screenSize.width-frame.size.width)/2 }
return CGFloat(0)
}
/// Returns the center y-point of the `screen.visibleFrame` (the frame between the Status Bar and Dock).
/// Falls back on `screen.frame` when `.visibleFrame` is unavailable (includes Status Bar and Dock).
public func yCenter() -> CGFloat {
if let screenSize = screen?.visibleFrame.size { return (screenSize.height-frame.size.height)/2 }
if let screenSize = screen?.frame.size { return (screenSize.height-frame.size.height)/2 }
return CGFloat(0)
}
}
Usage
NSWindow
Positions the existing window to the center of visibleFrame.
window!.positionCenter()
Sets a new window frame, at the center of visibleFrame, with dimensions
window!.setCenterFrame(width: 900, height: 600)
NSView
Using xCenter() and yCenter() to get the central x-y points of the visibleFrame.
let x = self.view.window?.xCenter() ?? CGFloat(0)
let y = self.view.window?.yCenter() ?? CGFloat(0)
self.view.window?.setFrame(NSRect(x: x, y: y, width: CGFloat(900), height: CGFloat(600)), display: true)
It would be awesome if some UI guru could comment on whether this is the "right" answer, but here's what I did as a wild guess based on "El Tomato's" rather cryptic comments. First, add the following to ViewController.swift:
class WildGuessWindowController: NSWindowController {
override func windowDidLoad() {
if let screenSize = window?.screen?.frame {
window!.setFrame(screenSize, display: true)
print(screenSize)
}
super.windowDidLoad()
}
}
Next, open Main.storyboard in the project navigator. You might see something like the following:
Click on the box that says "Window Controller", and open the right-hand side panel. You should see something like this.
Notice the box next to the arrow, that has a grayed-out class name in it. (Also notice that you might have to click the "Identity inspector" button, the blue one above where it says "Custom Class".) Now click that drop-down, and you should see WildGuessWindowController. Select that, and you're set. Build and run. The framework will instantiate your class, and run windowDidLoad().

Consistent curves with dynamic corner radius (Swift)?

Is there a way to make the corner radius of a UIView adept to the view it belongs to? I'm not comfortable with the idea of hard-coding corner radius values, because as soon as the width or the height of your view changes (on different screen orientations for example), the corners will look totally different. For example, take a look at WhatsApp's chat window.
As you can see, every message container view has a different width and a different height, but the curve of the corners are all exactly the same. This is what I'm trying to achieve. I want the curves of my corners to be the same on every view, no matter what the size of the view is or what screen the view is displayed on. I've tried setting the corner radius relative to the view's height (view.layer.cornerRadius = view.frame.size.height * 0.25) and I've also tried setting it to the view's width, but this doesn't work. The corners still look weird as soon as they are displayed on a different screen size. Please let me know if there's a certain formula or trick to make the curves look the same on every view/screen size.
Here's the best I can do. I don't know if this will be of help, but hopefully it will give you some ideas.
First the code:
class ViewController: UIViewController {
let cornerRadius:CGFloat = 10
let insetValue:CGFloat = 10
var numberOfViews:Int = 0
var myViews = [UIView]()
override func viewDidLoad() {
super.viewDidLoad()
view.translatesAutoresizingMaskIntoConstraints = false
}
override func viewWillLayoutSubviews() {
setNumberOfViews()
createViews()
createViewHierarchy()
addConstraints()
}
func setNumberOfViews() {
var smallerDimension:CGFloat = 0
if view.frame.height < view.frame.width {
smallerDimension = view.frame.height
} else {
smallerDimension = view.frame.width
}
let viewCount = smallerDimension / (insetValue * 2)
numberOfViews = Int(viewCount)
}
func createViews() {
for i in 1...numberOfViews {
switch i % 5 {
case 0:
myViews.append(MyView(UIColor.black, cornerRadius))
case 1:
myViews.append(MyView(UIColor.blue, cornerRadius))
case 2:
myViews.append(MyView(UIColor.red, cornerRadius))
case 3:
myViews.append(MyView(UIColor.yellow, cornerRadius))
case 4:
myViews.append(MyView(UIColor.green, cornerRadius))
default:
break
}
}
}
func createViewHierarchy() {
view.addSubview(myViews[0])
for i in 1...myViews.count-1 {
myViews[i-1].addSubview(myViews[i])
}
}
func addConstraints() {
for view in myViews {
view.topAnchor.constraint(equalTo: (view.superview?.topAnchor)!, constant: insetValue).isActive = true
view.leadingAnchor.constraint(equalTo: (view.superview?.leadingAnchor)!, constant: insetValue).isActive = true
view.trailingAnchor.constraint(equalTo: (view.superview?.trailingAnchor)!, constant: -insetValue).isActive = true
view.bottomAnchor.constraint(equalTo: (view.superview?.bottomAnchor)!, constant: -insetValue).isActive = true
}
}
}
class MyView: UIView {
convenience init(_ backgroundColor:UIColor, _ cornerRadius:CGFloat) {
self.init(frame: CGRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = backgroundColor
self.layer.cornerRadius = cornerRadius
}
}
Explanation:
This is fairly simple code. The intent was to create as deeply nested a view hierarchy as possible, and, using auto layout, have two main variables: cornerRadius (the view's corner radius) and insetValue (the "frame's" inset). These two variables can be adjusted for experimenting.
The bulk of the logic is in viewWillLayoutSubviews, where the root view frame size is know. Since I'm using 5 different background colors, I'm calculating how many views can fit in the hierarchy. Then I'm creating them, followed by creating the view hierarchy, and finally I'm adding the constraints.
Experimenting and conclusions:
I was able to see what your concern is - yes, if a view's size components are smaller than the corner radius, you end up with inconsistent looking corners. But these values are pretty small - pretty much 10 or less. Most views are unusable at that size. (If I recall even the HIG suggests that a button should be no less than 40 points in size. Sure, even Apple breaks that rule. Still.)
If your 'insetValueis sufficiently larger than the corner radius, you should never have an issue. Likewise, using the iMessage scenario, a singleUILabelcontaining text and/or emoticons should have enough height that a noticeablecornerRadius` can be had.
The key point to set things like cornerRadius and insetValue is in viewWillLayoutSubviews, when you can decide (1) which is the smaller dimension, height or width, (2) how deeply you can nest views, and (3) how large of a corner radius you can set.
Use auto layout! Please note the absolute lack of frames. Other than determining the root view's dimensions at the appropriate time, you can write very compact code without worrying about device size or orientation.