The text in UITextView is being cutoff when rotated - ios

I am trying to allow the user to rotate text they place on a view inside UITextViews. The problem is when I rotate them using the transform property, the text gets cutoff. However, when I do this with UILabels, it works fine, so why not UITextViews? The bounds of UITextView (shown in green) never changes, so I am not sure why this is happening. The amount of cutoff seems related to the height of the text view.
The only work around I've found is to do the transform asynchronously. I don't know why it works, but I don't think it's a good solution. You can see this in action with the following code.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
for xPos in stride(from: 80, to: view.bounds.width, by: 80) {
let point = CGPoint(x: xPos, y: 100)
let textView = makeTextView(at: point)
//DispatchQueue.main.async {
let angle = xPos / 400.0
textView.transform = CGAffineTransform(rotationAngle: angle)
//}
view.addSubview(textView)
}
}
private func makeTextView(at point: CGPoint) -> UITextView {
let textView = UITextView()
textView.text = "1234567890"
textView.isScrollEnabled = false
textView.backgroundColor = .green
textView.center = CGPoint(x: point.x, y: point.y)
textView.bounds.size = CGSize(width: 80, height: 24)
return textView
}
}
What is the correct way to rotate them?

Related

How To Scale The Contents Of A UIView To Fit A Destination Rectangle Whilst Maintaining The Aspect Ratio?

I am trying to solve a problem without success and am hoping someone could help.
I have looked for similar posts but haven't been able to find anything which solves my problem.
My Scenario is as follows:
I have a UIView on which a number of other UIViews can be placed. These can be moved, scaled and rotated using gesture recognisers (There is no issue here).
The User is able to change the Aspect Ratio of the Main View (the Canvas) and my problem is trying to scale the content of the Canvas to fit into the new destination size.
There are a number of posts with a similar theme e.g:
calculate new size and location on a CGRect
How to create an image of specific size from UIView
But these don't address the changing of ratios multiple times.
My Approach:
When I change the aspect ratio of the canvas, I make use of AVFoundation to calculate an aspect fitted rectangle which the subviews of the canvas should fit:
let sourceRectangleSize = canvas.frame.size
canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
view.layoutIfNeeded()
let destinationRectangleSize = canvas.frame.size
let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
ratioVisualizer.frame = aspectFittedFrame
The Red frame is simply to visualise the Aspect Fitted Rectangle. As you can see whilst the aspect fitted rectangle is correct, the scaling of objects isn't working. This is especially true when I apply scale and rotation to the subviews (CanvasElement).
The logic where I am scaling the objects is clearly wrong:
#objc
private func setRatio(_ control: UISegmentedControl) {
guard let aspect = Aspect(rawValue: control.selectedSegmentIndex) else { return }
let sourceRectangleSize = canvas.frame.size
canvas.setAspect(aspect, screenSize: editorLayoutGuide.layoutFrame.size)
view.layoutIfNeeded()
let destinationRectangleSize = canvas.frame.size
let aspectFittedFrame = AVMakeRect(aspectRatio:sourceRectangleSize, insideRect: CGRect(origin: .zero, size: destinationRectangleSize))
ratioVisualizer.frame = aspectFittedFrame
let scale = min(aspectFittedFrame.size.width/canvas.frame.width, aspectFittedFrame.size.height/canvas.frame.height)
for case let canvasElement as CanvasElement in canvas.subviews {
canvasElement.frame.size = CGSize(
width: canvasElement.baseFrame.width * scale,
height: canvasElement.baseFrame.height * scale
)
canvasElement.frame.origin = CGPoint(
x: aspectFittedFrame.origin.x + canvasElement.baseFrame.origin.x * scale,
y: aspectFittedFrame.origin.y + canvasElement.baseFrame.origin.y * scale
)
}
}
I am enclosing the CanvasElement Class as well if this helps:
final class CanvasElement: UIView {
var rotation: CGFloat = 0
var baseFrame: CGRect = .zero
var id: String = UUID().uuidString
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
storeState()
setupGesture()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// MARK: - Gesture Setup
private func setupGesture() {
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:)))
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(pinchGesture(_:)))
let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(rotateGesture(_:)))
addGestureRecognizer(panGestureRecognizer)
addGestureRecognizer(pinchGestureRecognizer)
addGestureRecognizer(rotateGestureRecognizer)
}
// MARK: - Touches
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
moveToFront()
}
//MARK: - Gestures
#objc
private func panGesture(_ sender: UIPanGestureRecognizer) {
let move = sender.translation(in: self)
transform = transform.concatenating(.init(translationX: move.x, y: move.y))
sender.setTranslation(CGPoint.zero, in: self)
storeState()
}
#objc
private func pinchGesture(_ sender: UIPinchGestureRecognizer) {
transform = transform.scaledBy(x: sender.scale, y: sender.scale)
sender.scale = 1
storeState()
}
#objc
private func rotateGesture(_ sender: UIRotationGestureRecognizer) {
rotation += sender.rotation
transform = transform.rotated(by: sender.rotation)
sender.rotation = 0
storeState()
}
// MARK: - Miscelaneous
func moveToFront() {
superview?.bringSubviewToFront(self)
}
public func rotated(by degrees: CGFloat) {
transform = transform.rotated(by: degrees)
rotation += degrees
}
func storeState() {
print("""
Element Frame = \(frame)
Element Bounds = \(bounds)
Element Center = \(center)
""")
baseFrame = frame
}
}
Any help or advise, approaches, with some actual examples would be great. Im not expecting anyone to provide full source code, but something which I could use as a basis.
Thank you for taking the time to read my question.
Here are a few thoughts and findings while playing around with this
1. Is the right scale factor being used?
The scaling you use is a bit custom and cannot be compared directly to the examples which has just 1 scale factor like 2 or 3. However, your scale factor has 2 dimensions but I see you compensate for this to get the minimum of the width and height scaling:
let scale = min(aspectFittedFrame.size.width / canvas.frame.width,
aspectFittedFrame.size.height / canvas.frame.height)
In my opinion, I don't think this is the right scale factor. To me this compares new aspectFittedFrame with the new canvas frame
when actually I believe the right scaling factor is to compare the new aspectFittedFrame with the previous canvas frame
let scale
= min(aspectFittedFrame.size.width / sourceRectangleSize.width,
aspectFittedFrame.size.height / sourceRectangleSize.height)
2. Is the scale being applied on the right values?
If you notice, the first order from 1:1 to 16:9 works quite well. However after that it does not seem to work and I believe the issue is here:
for case let canvasElement as CanvasElement in strongSelf.canvas.subviews
{
canvasElement.frame.size = CGSize(
width: canvasElement.baseFrame.width * scale,
height: canvasElement.baseFrame.height * scale
)
canvasElement.frame.origin = CGPoint(
x: aspectFittedFrame.origin.x
+ canvasElement.baseFrame.origin.x * scale,
y: aspectFittedFrame.origin.y
+ canvasElement.baseFrame.origin.y * scale
)
}
The first time, the scale works well because canvas and the canvas elements are being scaled in sync or mapped properly:
However, if you go beyond that, because you are always scaling based on the base values your aspect ratio frame and your canvas elements are out of sync
So in the example of 1:1 -> 16:9 -> 3:2
Your viewport has been scaled twice 1:1 -> 16:9 and from 16:9 -> 3:2
Whereas your elements are scaled once each time, 1:1 -> 16:9 and 1:1 -> 3:2 because you always scale from the base values
So I feel to see the values within the red viewport, you need to apply the same continuous scaling based on the previous view rather than the base view.
Just for an immediate quick fix, I update the base values of the canvas element after each change in canvas size by calling canvasElement.storeState():
for case let canvasElement as CanvasElement in strongSelf.canvas.subviews
{
canvasElement.frame.size = CGSize(
width: canvasElement.baseFrame.width * scale,
height: canvasElement.baseFrame.height * scale
)
canvasElement.frame.origin = CGPoint(
x: aspectFittedFrame.origin.x
+ canvasElement.baseFrame.origin.x * scale,
y: aspectFittedFrame.origin.y
+ canvasElement.baseFrame.origin.y * scale
)
// I added this
canvasElement.storeState()
}
The result is perhaps closer to what you want ?
Final thoughts
While this might fix your issue, you will notice that it is not possible to come back to the original state as at each step a transformation is applied.
A solution could be to store the current values mapped to a specific viewport aspect ratio and calculate the right sizes for the others so that if you needed to get back to the original, you could do that.
Couple suggestions...
First, when using your CanvasElement, panning doesn't work correctly if the view has been rotated.
So, instead of using a translate transform to move the view, change the .center itself. In addition, when panning, we want to use the translation in the superview, not in the view itself:
#objc
func panGesture(_ gest: UIPanGestureRecognizer) {
// change the view's .center instead of applying translate transform
// use translation in superview, not in self
guard let superV = superview else { return }
let translation = gest.translation(in: superV)
center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
gest.setTranslation(CGPoint.zero, in: superV)
}
Now, when we want to scale the subviews when the "Canvas" changes size, we can do this...
We'll track the "previous" bounds and use the "new bounds" to calculate the scale:
let newBounds: CGRect = bounds
let scW: CGFloat = newBounds.size.width / prevBounds.size.width
let scH: CGFloat = newBounds.size.height / prevBounds.size.height
for case let v as CanvasElement in subviews {
// reset transform before scaling / positioning
let tr = v.transform
v.transform = .identity
let w = v.frame.width * scW
let h = v.frame.height * scH
let cx = v.center.x * scW
let cy = v.center.y * scH
v.frame.size = CGSize(width: w, height: h)
v.center = CGPoint(x: cx, y: cy)
// re-apply transform
v.transform = tr
}
prevBounds = newBounds
Here's a complete sample implementation. Please note: this is Example Code Only!!! It is not intended to be "Production Ready."
import UIKit
// MARK: enum to provide strings and aspect ratio values
enum Aspect: Int, Codable, CaseIterable {
case a1to1
case a16to9
case a3to2
case a4to3
case a9to16
var stringValue: String {
switch self {
case .a1to1:
return "1:1"
case .a16to9:
return "16:9"
case .a3to2:
return "3:2"
case .a4to3:
return "4:3"
case .a9to16:
return "9:16"
}
}
var aspect: CGFloat {
switch self {
case .a1to1:
return 1
case .a16to9:
return 9.0 / 16.0
case .a3to2:
return 2.0 / 3.0
case .a4to3:
return 3.0 / 4.0
case .a9to16:
return 16.0 / 9.0
}
}
}
class EditorView: UIView {
// no code -
// just makes it easier to identify
// this view when debugging
}
// CanvasElement views will be added as subviews
// this handles the scaling / positioning when the bounds changes
// also (optionally) draws a grid (for use during development)
class CanvasView: UIView {
public var showGrid: Bool = true
private let gridLayer: CAShapeLayer = CAShapeLayer()
private var prevBounds: CGRect = .zero
// MARK: init
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
gridLayer.fillColor = UIColor.clear.cgColor
gridLayer.strokeColor = UIColor.red.cgColor
gridLayer.lineWidth = 1
layer.addSublayer(gridLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
// MARK: 10 x 10 grid
if showGrid {
// draw a grid on the inside of the bounds
// so the edges are not 1/2 point width
let gridBounds: CGRect = bounds.insetBy(dx: 0.5, dy: 0.5)
let path: UIBezierPath = UIBezierPath()
let w: CGFloat = gridBounds.width / 10.0
let h: CGFloat = gridBounds.height / 10.0
var p: CGPoint = .zero
p = CGPoint(x: gridBounds.minX, y: gridBounds.minY)
for _ in 0...10 {
path.move(to: p)
path.addLine(to: CGPoint(x: p.x, y: gridBounds.maxY))
p.x += w
}
p = CGPoint(x: gridBounds.minX, y: gridBounds.minY)
for _ in 0...10 {
path.move(to: p)
path.addLine(to: CGPoint(x: gridBounds.maxX, y: p.y))
p.y += h
}
gridLayer.path = path.cgPath
}
// MARK: update subviews
// we only want to move/scale the subviews if
// the bounds has > 0 width and height and
// prevBounds has > 0 width and height and
// the bounds has changed
guard bounds != prevBounds,
bounds.width > 0, prevBounds.width > 0,
bounds.height > 0, prevBounds.height > 0
else { return }
let newBounds: CGRect = bounds
let scW: CGFloat = newBounds.size.width / prevBounds.size.width
let scH: CGFloat = newBounds.size.height / prevBounds.size.height
for case let v as CanvasElement in subviews {
// reset transform before scaling / positioning
let tr = v.transform
v.transform = .identity
let w = v.frame.width * scW
let h = v.frame.height * scH
let cx = v.center.x * scW
let cy = v.center.y * scH
v.frame.size = CGSize(width: w, height: h)
v.center = CGPoint(x: cx, y: cy)
// re-apply transform
v.transform = tr
}
prevBounds = newBounds
}
override var bounds: CGRect {
willSet {
prevBounds = bounds
}
}
}
// self-contained Pan/Pinch/Rotate view
// set allowSimultaneous to TRUE to enable
// simultaneous gestures
class CanvasElement: UIView, UIGestureRecognizerDelegate {
public var allowSimultaneous: Bool = false
// MARK: init
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
isUserInteractionEnabled = true
isMultipleTouchEnabled = true
let panG = UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:)))
let pinchG = UIPinchGestureRecognizer(target: self, action: #selector(pinchGesture(_:)))
let rotateG = UIRotationGestureRecognizer(target: self, action: #selector(rotateGesture(_:)))
[panG, pinchG, rotateG].forEach { g in
g.delegate = self
addGestureRecognizer(g)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
// unwrap optional superview
guard let superV = superview else { return }
superV.bringSubviewToFront(self)
}
// MARK: UIGestureRecognizer Methods
#objc
func panGesture(_ gest: UIPanGestureRecognizer) {
// change the view's .center instead of applying translate transform
// use translation in superview, not in self
guard let superV = superview else { return }
let translation = gest.translation(in: superV)
center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
gest.setTranslation(CGPoint.zero, in: superV)
}
#objc
func pinchGesture(_ gest: UIPinchGestureRecognizer) {
// apply scale transform
transform = transform.scaledBy(x: gest.scale, y: gest.scale)
gest.scale = 1
}
#objc
func rotateGesture(_ gest : UIRotationGestureRecognizer) {
// apply rotate transform
transform = transform.rotated(by: gest.rotation)
gest.rotation = 0
}
// MARK: UIGestureRecognizerDelegate Methods
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return allowSimultaneous
}
}
// example view controller
// Aspect Ratio segmented control
// changes the Aspect Ratio of the Editor View
// includes triple-tap gesture to cycle through
// 3 "starting subview" layouts
class ViewController: UIViewController, UIGestureRecognizerDelegate {
let editorView: EditorView = {
let v = EditorView()
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let canvasView: CanvasView = {
let v = CanvasView()
v.backgroundColor = .yellow
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
// segmented control for selecting Aspect Ratio
let aspectRatioSeg: UISegmentedControl = {
let v = UISegmentedControl()
v.setContentCompressionResistancePriority(.required, for: .vertical)
v.setContentHuggingPriority(.required, for: .vertical)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
// this will be changed by the Aspect Ratio segmented control
var evAspectConstraint: NSLayoutConstraint!
// used to cycle through intitial subviews layout
var layoutMode: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.99, alpha: 1.0)
// container view for laying out editor view
let containerView: UIView = {
let v = UIView()
v.backgroundColor = .cyan
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
// setup the aspect ratio segmented control
for (idx, m) in Aspect.allCases.enumerated() {
aspectRatioSeg.insertSegment(withTitle: m.stringValue, at: idx, animated: false)
}
// add canvas view to editor view
editorView.addSubview(canvasView)
// add editor view to container view
containerView.addSubview(editorView)
// add container view to self's view
view.addSubview(containerView)
// add UI Aspect Ratio segmented control to self's view
view.addSubview(aspectRatioSeg)
// always respect the safe area
let safeG = view.safeAreaLayoutGuide
// editor view inset from container view sides
let evInset: CGFloat = 0
// canvas view inset from editor view sides
let cvInset: CGFloat = 0
// these sets of constraints will make the Editor View and the Canvas View
// as large as their superviews (with "Inset Edge Padding" if set above)
// while maintaining aspect ratios and centering
let evMaxW = editorView.widthAnchor.constraint(lessThanOrEqualTo: containerView.widthAnchor, constant: -evInset)
let evMaxH = editorView.heightAnchor.constraint(lessThanOrEqualTo: containerView.heightAnchor, constant: -evInset)
let evW = editorView.widthAnchor.constraint(equalTo: containerView.widthAnchor)
let evH = editorView.heightAnchor.constraint(equalTo: containerView.heightAnchor)
evW.priority = .required - 1
evH.priority = .required - 1
let cvMaxW = canvasView.widthAnchor.constraint(lessThanOrEqualTo: editorView.widthAnchor, constant: -cvInset)
let cvMaxH = canvasView.heightAnchor.constraint(lessThanOrEqualTo: editorView.heightAnchor, constant: -cvInset)
let cvW = canvasView.widthAnchor.constraint(equalTo: editorView.widthAnchor)
let cvH = canvasView.heightAnchor.constraint(equalTo: editorView.heightAnchor)
cvW.priority = .required - 1
cvH.priority = .required - 1
// editor view starting aspect ratio
// this is changed by the segmented control
let editorAspect: Aspect = .a1to1
aspectRatioSeg.selectedSegmentIndex = editorAspect.rawValue
evAspectConstraint = editorView.heightAnchor.constraint(equalTo: editorView.widthAnchor, multiplier: editorAspect.aspect)
// we can set the Aspect Ratio of the CanvasView here
// it will maintain its Aspect Ratio independent of
// the Editor View's Aspect Ratio
let canvasAspect: Aspect = .a1to1
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: safeG.topAnchor),
containerView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor),
editorView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
editorView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
evMaxW, evMaxH,
evW, evH,
evAspectConstraint,
canvasView.centerXAnchor.constraint(equalTo: editorView.centerXAnchor),
canvasView.centerYAnchor.constraint(equalTo: editorView.centerYAnchor),
cvMaxW, cvMaxH,
cvW, cvH,
canvasView.heightAnchor.constraint(equalTo: canvasView.widthAnchor, multiplier: canvasAspect.aspect),
aspectRatioSeg.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 8.0),
aspectRatioSeg.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -8.0),
aspectRatioSeg.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
aspectRatioSeg.widthAnchor.constraint(greaterThanOrEqualTo: safeG.widthAnchor, multiplier: 0.5),
aspectRatioSeg.widthAnchor.constraint(lessThanOrEqualTo: safeG.widthAnchor),
])
aspectRatioSeg.addTarget(self, action: #selector(aspectRatioSegmentChanged(_:)), for: .valueChanged)
// triple-tap anywhere to "reset" the 3 subviews
// cycling between starting sizes/positions
let tt = UITapGestureRecognizer(target: self, action: #selector(resetCanvas))
tt.numberOfTapsRequired = 3
tt.delaysTouchesEnded = false
view.addGestureRecognizer(tt)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// we don't have the frames in viewDidLoad,
// so wait until now to add the CanvasElement views
resetCanvas()
}
#objc func resetCanvas() {
canvasView.subviews.forEach { v in
v.removeFromSuperview()
}
// add 3 views to the canvas
let v1 = CanvasElement()
v1.backgroundColor = .systemYellow
let v2 = CanvasElement()
v2.backgroundColor = .systemGreen
let v3 = CanvasElement()
v3.backgroundColor = .systemBlue
// default size of subviews is 2/10ths the width of the canvas
let w: CGFloat = canvasView.bounds.width * 0.2
[v1, v2, v3].forEach { v in
v.frame = CGRect(x: 0, y: 0, width: w, height: w)
canvasView.addSubview(v)
// if we want to allow simultaneous gestures
// i.e. pan/scale/rotate all at the same time
//v.allowSimultaneous = true
}
switch (layoutMode % 3) {
case 1:
// top-left corner
// center at 1.5 times the size
// bottom-right corner
v1.frame.origin = CGPoint(x: 0, y: 0)
v2.frame.size = CGSize(width: w * 1.5, height: w * 1.5)
v2.center = CGPoint(x: canvasView.bounds.midX, y: canvasView.bounds.midY)
v3.center = CGPoint(x: canvasView.bounds.maxX - w * 0.5, y: canvasView.bounds.maxY - w * 0.5)
()
case 2:
// different sized views
v1.frame = CGRect(x: 0, y: 0, width: w * 0.5, height: w)
v2.frame.size = CGSize(width: w, height: w)
v2.center = CGPoint(x: canvasView.bounds.midX, y: canvasView.bounds.midY)
v3.frame.size = CGSize(width: w, height: w * 0.5)
v3.center = CGPoint(x: canvasView.bounds.maxX - v3.frame.width * 0.5, y: canvasView.bounds.maxY - v3.frame.height * 0.5)
()
default:
// on a "diagonal"
// starting at top-left corner
v1.frame.origin = CGPoint(x: 0, y: 0)
v2.frame.origin = CGPoint(x: w, y: w)
v3.frame.origin = CGPoint(x: w * 2, y: w * 2)
()
}
layoutMode += 1
}
#objc func aspectRatioSegmentChanged(_ sender: Any?) {
if let seg = sender as? UISegmentedControl,
let r = Aspect.init(rawValue: seg.selectedSegmentIndex)
{
evAspectConstraint.isActive = false
evAspectConstraint = editorView.heightAnchor.constraint(equalTo: editorView.widthAnchor, multiplier: r.aspect)
evAspectConstraint.isActive = true
}
}
}
Some sample screenshots...
Yellow is the Canvas view... with optional red 10x10 grid
Gray is the Editor view... this is the view that changes Aspect Ratio
Cyan is the "Container" view.... Editor view fits/centers itself
Note that the Canvas view can be set to something other than a square (1:1 ratio). For example, here it's set to 9:16 ratio -- and maintains its Aspect Ratio independent of the Editor view Aspect Ratio:
With this example controller, triple-tap anywhere to cycle through 3 "starting layouts":
Maybe you can make the three rectangles in a view. And then you can keep the aspect-ratio for the view.
If you are using autolayout and Snapkit. The constrains maybe like this:
view.snp.makeConstraints { make in
make.width.height.lessThanOrEqualToSuperview()
make.centerX.centerY.equalToSuperview()
make.width.equalTo(view.snp.height)
make.width.height.equalToSuperview().priority(.high)
}
So this view will be aspect-fit in superview.
Back to children in this view. If you want to scale every child when view's frame changed, you should add contrains too. Or you can use autoresizingMask, it maybe simpler.
If you didn't want to use autolayout. Maybe you can try transform. When you transform some view, the children in this view will be changed too.
// The scale depends on the aspect-ratio of superview.
view.transform = CGAffineTransformMakeScale(0.5, 0.5);

How can we achieve dynamic items with dynamic content in custom table cell?

How can I achieve the dynamic no of items with dynamic content size one after other and in next line if the content gets more than superview in a custom table view cell?
Please refer image for question clarification.
The question you posted is a bit long and actually requests for multiple things since you didn't post what you have tried and what part you are having trouble with. There are many ways of doing this so let's start with normal label:
An UILabel can have attributed string under attributedText which can be used to achieve the visual design you are showing in your image. To lay them out horizontally we could use sizeToFit to get their width. Assume something like:
func layoutLabels(_ labels: [UILabel], inView view: UIView, horizontalSeparator: CGFloat = 4.0) {
var x: CGFloat = 0.0
labels.forEach { label in
label.sizeToFit() // Will make it's size just right
label.frame = CGRect(x: x, y: 0.0, width: label.frame.width, height: label.frame.height)
x = label.frame.maxX + horizontalSeparator
view.addSubview(label)
}
}
Now to add vertical part we simply need to add maximum width and also add y component:
func layoutLabels(_ labels: [UILabel], inView view: UIView, maximumWidth: CGFloat, lineHeight: CGFloat, horizontalSeparator: CGFloat = 4.0, verticalSeparator: CGFloat = 2.0) {
var x: CGFloat = 0.0
var y: CGFloat = 0.0
labels.forEach { label in
label.sizeToFit() // Will make it's size just right
var newX = x + label.frame.width
if newX > maximumWidth {
if x == 0.0 {
// We are at the beginning of the line and a single label is simply too large. We will need to reduce it's width
label.frame = CGRect(x: x, y: y, width: maximumWidth, height: label.frame.height)
} else {
// We should go into new line
x = 0.0
y += lineHeight
label.frame = CGRect(x: x, y: y, width: min(label.frame.width, maximumWidth), height: label.frame.height)
newX = x + label.frame.width
}
} else {
label.frame = CGRect(x: x, y: y, width: min(label.frame.width, maximumWidth), height: label.frame.height)
}
x = newX + horizontalSeparator
view.addSubview(label)
}
}
This should now let you fill labels the way as on your image. We are also checking for line breaks and labels that can not fit overall width of your view. So now we need to define what is the actual size of your view. y should return that but we need to be able to use it in a table view cell. So we can simply return it:
func layoutLabels(_ labels: [UILabel], inView view: UIView, maximumWidth: CGFloat, lineHeight: CGFloat, horizontalSeparator: CGFloat = 4.0, verticalSeparator: CGFloat = 2.0) -> CGFloat {
...
return labels.count > 0 ? y + lineHeight : 0.0
}
Now in your table view cell it might be best to use some constraint outlet:
protocol Tag {
func createLabel() -> UILabel
}
class Cell: UITableViewCell {
#IBOutlet private var tagsPanel: UIView!
#IBOutlet private var tagsPanelHeightConstraint: NSLayoutConstraint?
var tags: [Tag]!
func refresh() {
tagsPanel.subviews.forEach { $0.removeFromSuperview() } // Ugly, needs improvement
let height = layoutLabels(tags.map { $0.createLabel() }, inView: tagsPanel, maximumWidth: tagsPanel.frame.width, lineHeight: 40.0)
tagsPanelHeightConstraint?.constant = height
}
}
Now the problem is that your table view needs to refresh because your table view cell might have resized. You will need to have a reference to your table view and simple call begin/end updates on it.
var tags: [Tag]!
weak var tableView: UITableView? // Assigned in cellForRowAtIndexPath
func refresh() {
tagsPanel.subviews.forEach { $0.removeFromSuperview() } // Ugly, needs improvement
let height = layoutLabels(tags.map { $0.createLabel() }, inView: tagsPanel, maximumWidth: tagsPanel.frame.width, lineHeight: 40.0)
if let tagsPanelHeightConstraint = tagsPanelHeightConstraint, tagsPanelHeightConstraint.constant != height {
tagsPanelHeightConstraint.constant = height
tableView?.beginUpdates()
tableView?.endUpdates()
}
}
Good luck.

Auto-size view with dynamic font in enclosed textview

So here's one I just can't seem to find a matching case for in searching on here.
I have a small UIView that contains a UITextView, and the UIView needs to auto-size around the TextView for presentation over another view. Basically the TextView needs to fully fill the UIView, and the UIView should only be big enough to contain the TextView.
The TextView just contains a couple sentences that are meant to stay on the screen until an external thing happens, and certain values change.
Everything is great when I used a fixed-size font.
But hey... I'm an old guy, and I have the text size jacked up a bit on my phone. Testing it on my device shows where I must be missing something.
When using the dynamic font style "Title 2" in the textview properties, and turning on "Automatically adjust font" in the TextView properties, and having the text larger than the default, it seems as if I'm not properly capturing the size of the TextView's growth (with the bigger text) when creating the new bounding rect to toss at the frame. It's returning values that look a lot like the smaller, default-size text values rather than the increased text size.
Code is below, the view's class code as well as the calling code (made super explicit for posting here). I figure I'm either missing something silly like capturing the size after something happens to the fonts, but even moving this code to a new function and explicitly calling it after the controls fully draw doesn't seem to do it.
I hope this make sense.
Thanks, all.
Calling code:
let noWView:NoWitnessesYetView = (Bundle.main.loadNibNamed("NoWitnessesYetView", owner: nil, options: nil)!.first as! NoWitnessesYetView)
//if nil != noWView {
let leftGutter:CGFloat = 20.0
let bottomGutter:CGFloat = 24.0
let newWidth = self.view.frame.width - ( leftGutter + leftGutter )
let newTop = (eventMap.frame.minY + eventMap.frame.height) - ( noWView.frame.height + bottomGutter ) // I suspect here is the issue
// I suspect that loading without drawing is maybe not allowing
// the fonts to properly draw and the
// TextView to figure out the size...?
noWView.frame = CGRect(x: 20, y: newTop, width: newWidth, height: noWView.frame.height)
self.view.addSubview(noWView)
//}
Class code:
import UIKit
class NoWitnessesYetView: UIView {
#IBOutlet weak var textView: EyeneedRoundedTextView!
override func draw(_ rect: CGRect) {
let newWidth = self.frame.width
// form up a dummy size just to get the proper height for the popup
let workingSize:CGSize = self.textView.sizeThatFits(CGSize(width: newWidth, height: CGFloat(MAXFLOAT)))
// then build the real newSize value
let newSize = CGSize(width: newWidth, height: workingSize.height)
textView.frame.size = newSize
self.textView.isHidden = false
}
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = UIColor.clear // .blue
self.layer.cornerRadius = 10
}
}
This perfect way to do it the content comes from : https://www.youtube.com/watch?v=0Jb29c22xu8 .
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// let's create our text view
let textView = UITextView()
textView.frame = CGRect(x: 0, y: 0, width: 200, height: 100)
textView.backgroundColor = .lightGray
textView.text = "Here is some default text that we want to show and it might be a couple of lines that are word wrapped"
view.addSubview(textView)
// use auto layout to set my textview frame...kinda
textView.translatesAutoresizingMaskIntoConstraints = false
[
textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
textView.heightAnchor.constraint(equalToConstant: 50)
].forEach{ $0.isActive = true }
textView.font = UIFont.preferredFont(forTextStyle: .headline)
textView.delegate = self
textView.isScrollEnabled = false
textViewDidChange(textView)
}
}
extension ViewController: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
print(textView.text)
let size = CGSize(width: view.frame.width, height: .infinity)
let estimatedSize = textView.sizeThatFits(size)
textView.constraints.forEach { (constraint) in
if constraint.firstAttribute == .height {
constraint.constant = estimatedSize.height
}
}
}
}

How to make multi-lined, underlined UITextView/UITextField?

I'd like a multi-line text entry field with the appearance of lined paper, where each entry line has an underline beneath where the text would go spanning the width of the view.
For example:
This is the design I want to make. The number of lines could change.
I considered:
Using UITextView and placing a lined background.
Using TextField and underlining each textField.
None of these sound sane to me, so I'm looking for other ideas that could get me pointed in the right direction.
You can draw the lines using CoreGraphics:
class UnderlinedTextView: UITextView {
var lineHeight: CGFloat = 13.8
override var font: UIFont? {
didSet {
if let newFont = font {
lineHeight = newFont.lineHeight
}
}
}
override func draw(_ rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()
ctx?.setStrokeColor(UIColor.black.cgColor)
let numberOfLines = Int(rect.height / lineHeight)
let topInset = textContainerInset.top
for i in 1...numberOfLines {
let y = topInset + CGFloat(i) * lineHeight
let line = CGMutablePath()
line.move(to: CGPoint(x: 0.0, y: y))
line.addLine(to: CGPoint(x: rect.width, y: y))
ctx?.addPath(line)
}
ctx?.strokePath()
super.draw(rect)
}
}
Add textview delegate , and then use this after confirming to UITextViewDelegate delegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
yourTextView.setNeedsDisplay()
}
Hope it helps !

my initial problems with UIScrollView now appear to be related to autolayout

For my first challenge using UIScrollView I modified this example to make UIScrollView display not just another background colour but another UIView and UILabel on each page. But I could have just as easily chosen to display objects like UITableView, UIButton or UIImage.
Potentially, UIScrollView could be much more than a giant content view where users scroll from one part to the next, e.g., some pages might have a UIButton that takes a user to a specific page, the same way we use books.
Code Improvements
My question has evolved since I first posted it. Initially the labels piled up on page 1 (as shown below) but this has now been corrected. I also included this extension to make the font larger.
Further improvement ?
As the code evolved I became more aware of other issues e.g. iPhone 5 images (below) appear differently on iPhone 7 where the UILabel is centred but not the UIView. So my next challenge is possibly to learn how to combine UIScrollView with Autolayout. I invite anyone to spot other things that might be wrong.
ViewController.swift (corrected)
import UIKit
class ViewController: UIViewController,UIScrollViewDelegate {
let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
var views = [UIView]()
var lables = [UILabel]()
var colors:[UIColor] = [UIColor.red, UIColor.magenta, UIColor.blue, UIColor.cyan, UIColor.green, UIColor.yellow]
var frame: CGRect = CGRect.zero
var pageControl: UIPageControl = UIPageControl(frame: CGRect(x: 50, y: 500, width: 200, height: 50))
override func viewDidLoad() {
super.viewDidLoad()
initialiseViewsAndLables()
configurePageControl()
scrollView.delegate = self
self.view.addSubview(scrollView)
for index in 0..<colors.count {
frame.origin.x = self.scrollView.frame.size.width * CGFloat(index)
frame.size = self.scrollView.frame.size
self.scrollView.isPagingEnabled = true
views[index].frame = frame
views[index].backgroundColor = colors[Int(index)]
views[index].layer.cornerRadius = 20
views[index].layer.masksToBounds = true
lables[index].frame = frame
lables[index].center = CGPoint(x: (view.frame.midX + frame.origin.x), y: view.frame.midY)
lables[index].text = String(index + 1)
lables[index].defaultFont = UIFont(name: "HelveticaNeue", size: CGFloat(200))
lables[index].textAlignment = .center
lables[index].textColor = .black
let subView1 = views[index]
let subView2 = lables[index]
self.scrollView .addSubview(subView1)
self.scrollView .addSubview(subView2)
}
print(views, lables)
self.scrollView.contentSize = CGSize(width: self.scrollView.frame.size.width * CGFloat(colors.count), height: self.scrollView.frame.size.height)
pageControl.addTarget(self, action: Selector(("changePage:")), for: UIControlEvents.valueChanged)
}
func initialiseViewsAndLables() {
// Size of views[] and lables[] is linked to available colors
for index in 0..<colors.count {
views.insert(UIView(), at:index)
lables.insert(UILabel(), at: index)
}
}
func configurePageControl() {
// Total number of available pages is based on available colors
self.pageControl.numberOfPages = colors.count
self.pageControl.currentPage = 0
self.pageControl.backgroundColor = getColour()
self.pageControl.pageIndicatorTintColor = UIColor.black
self.pageControl.currentPageIndicatorTintColor = UIColor.green
self.view.addSubview(pageControl)
}
func getColour() -> UIColor {
let index = colors[pageControl.currentPage]
return (index)
}
func changePage(sender: AnyObject) -> () {
scrollView.setContentOffset(CGPoint(x: CGFloat(pageControl.currentPage) * scrollView.frame.size.width, y: 0), animated: true)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = round(scrollView.contentOffset.x / scrollView.frame.size.width)
pageControl.currentPage = Int(pageNumber)
pageControl.backgroundColor = getColour()
}
}
Extension
extension UILabel{
var defaultFont: UIFont? {
get { return self.font }
set { self.font = newValue }
}
}
The centre point of the lable on each frame must be offset by the origin of the content view (as Baglan pointed out). I've modified the following line of code accordingly.
lables[Int(index)].center = CGPoint(x: (view.frame.midX + frame.origin.x), y: view.frame.midY)

Resources