UITextView Horizontal & Vertical Scroll With Proper Cursor Positioning in Swift - swift

I am working on a project where I require to make UITextView horizontally scrollable.The problem statement is defined below
I have tried putting the UITextview within a ScrollView for horizontal scroll as suggested in other similar question solution on Stack Overflow, while the scroll works there are multiple issues related to cursor position like:
Dynamically setting the width of UITextView to match the width of content's biggest line (achievable)
Cursor doesn't show on the screen when content increase or decrease i.e while adding content in same line or deleting content in between, cursor position is not handled by UITextView perfectly the cursor jumps to line start and comes back to current position, and is not visible on screen.
UITextView should only be able to scroll Horizontally or Vertically.(Need to disable the diagonal scroll).
Attaching the current code I am experimenting with, also tried other answers in similar questions, doesn't work.:
class ViewController: UIViewController, UITextViewDelegate, UIScrollViewDelegate {
#IBOutlet weak var scroll:UIScrollView!
#IBOutlet weak var textView:UITextView!
var displayStr = ""
var strSize:CGRect!
var font:UIFont!
var maxSize:CGSize!
override func viewDidLoad() {
super.viewDidLoad()
displayStr = textView.text
textView.delegate = self
scroll.delegate = self
scroll.addSubview(textView)
textView.isEditable = true
textView.isScrollEnabled = false
maxSize = CGSize(width: 9999, height: 9999)
font = UIFont(name: "Menlo", size: 16)!
textView.font = font
updateWidth()
}
func updateWidth() {
strSize = (displayStr as NSString).boundingRect(with: maxSize, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font : font!], context: nil)
if strSize.width > self.view.frame.width {
textView.frame = CGRect(x: 0, y: 0, width: strSize.width + 50, height: view.frame.height+10)
}
scroll.frame = CGRect(x: 0, y: 100, width: view.frame.width, height: view.frame.height)
scroll.contentSize = CGSize(width: strSize.width + 30, height: strSize.height)
}
func textViewDidChange(_ textView: UITextView) {
updateWidth()
}
This is how my output looks

Related

Don't understand why my UILabel won't follow safeareaLayout constraints

Am not working with storyboards, and below is the full code for my UIViewController for my Main Menu screen. While everything appears to work, I made an error, but don't understand the outcome.
myView, the gray area is set to the safeareaLayout constraints
fillRects is a function where I prefill all the rects for the labels and buttons that I will place on myView
By accident, I passed the wrong view to fillRects, not myView, as intended. Therefore the UILabel I create below is larger than it should be.
But my understanding was that it should have been cropped since it is a child of myView, which is constrained to the safeAreaLayout guide. Yet from the included image, you can see that it goes beyond myView's area on the screen.
Is my error in the way I applied the safeareaLayout guides? Or my understanding as to how they work?
import UIKit
class MainMenuCtrl: UIViewController {
var viewBounds : CGRect = .zero
var topLabelRect : CGRect = .zero
var bottomLabelRect : CGRect = .zero
var menuRect : CGRect = .zero
private let myView : UIView = {
let myView = UIView()
myView.translatesAutoresizingMaskIntoConstraints = false
myView.backgroundColor = .gray
return myView
}()
override func viewDidLoad() {
super.viewDidLoad()
// Set background color func
setBGC(vc: view)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
view.backgroundColor = .green
view.addSubview(myView)
addContraints(main: view, child: myView)
////fill the CGRects for all the labels, and buttons
fillRects(vc: self)
let label = UILabel(frame: self.topLabelRect)
label.textAlignment = .center
label.backgroundColor = .red
label.text = "hello"
label.textColor = nameColor
label.font = .systemFont(ofSize: 40)
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.7
myView.addSubview(label)
}
override var prefersStatusBarHidden: Bool {
return false
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .darkContent
}
}
Here is the code for fillRects
func fillRects (vc: MainMenuCtrl) {
vc.viewBounds = vc.view.frame
vc.topLabelRect = CGRect(x: vc.viewBounds.minX, y: vc.viewBounds.minY,
width: vc.viewBounds.width, height: vc.viewBounds.height * 0.05)
vc.bottomLabelRect = CGRect(x: vc.viewBounds.minX, y: vc.viewBounds.height * 0.9,
width: vc.viewBounds.width, height: vc.viewBounds.height * 0.05)
vc.menuRect = CGRect(x: vc.viewBounds.minX, y: vc.viewBounds.height * 0.2,
width: vc.viewBounds.width, height: vc.viewBounds.height * 0.6)
}
A view has a clipToBounds property that dictates whether subViews are restricted to the bounds of their parent view. The default value for this is false, which explains the behaviour you are experiencing.
Setting view.clipToBounds = true on the parent view should result in the sub view behaving as you expected.

Swift - TextView that expands and stays in center

I am trying to accomplish an Idea I got in my head but I got stuck..
I need a TextView that expands in both ways: widht-height.
That has a minimum and maximum width, and minimum height.
That is centered in the middle of the parent (SCROLL) view.
And that has a button send at the bottom trailing part of the view.
Here's the idea:
So if the user types in the box then it expands in both directions. But there's a maximum width for it (so it doesn't go offscreen) but the height is not limited: due to the parent scrollview.
The problem is that the textView's height doesn't expand when text is breaking into new line.
Code:
func textViewDidChange(_ textView: UITextView) {
self.adjustTextViewFrames(textView: textView)
}
func adjustTextViewFrames(textView : UITextView){
var newSize = textView.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
if newSize.width > self.view.bounds.width - 20 {
newSize.width = self.view.bounds.width - (self.view.bounds.width/10)
}
messageBubbleTextViewWidthConstraint.constant = newSize.width
messageBubbleTextViewHeightConstraint.constant = newSize.height
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
Try This :
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
}
}
}
}
Credit goes to Brian Voong from Let's Build That App here link

How to add a Show More/Show Less UIButton to control UITextView

I'm using a UITextView to display some contents. Beneath the textView, I added a UIButton that can control if the TextView will display part or all of its content.
Ideally, when button tapped, the TextView will expand its Height to accommodate the length of the content.
I haven't found a good solution. I wonder if I should use UIlabel instead of textView.
You can make this thing working this way:
1. Add a UITextView and UIButton in Storyboard.
2. If you are using Autolayout Constraints, make an outlet of UITextView height.
3. In ViewController class make outlets as:
#IBOutlet var txtView: UITextView!
#IBOutlet var btn_Show: UIButton!
#IBOutlet var textView_HeightConstraint: NSLayoutConstraint!
4. Make a method that will give you height of UITextView according to the text in it.
func getRowHeightFromText(strText : String!) -> CGFloat
{
let textView : UITextView! = UITextView(frame: CGRect(x: self.txtView.frame.origin.x,
y: 0,
width: self.txtView.frame.size.width,
height: 0))
textView.text = strText
textView.font = UIFont(name: "Fira Sans", size: 16.0)
textView.sizeToFit()
var txt_frame : CGRect! = CGRect()
txt_frame = textView.frame
var size : CGSize! = CGSize()
size = txt_frame.size
size.height = 50 + txt_frame.size.height
return size.height
}
5. On Click of Button:
#IBAction func showMore_Button_Clicked(_ sender: UIButton)
{
if sender.tag == 0
{
let height = self.getRowHeightFromText(strText: self.txtView.text)
self.textView_HeightConstraint.constant = height
self.view.layoutIfNeeded()
btn_Show.setTitle("ShowLess", for: .normal)
sender.tag = 1
}
else
{
self.textView_HeightConstraint.constant = 116
self.view.layoutIfNeeded()
btn_Show.setTitle("ShowMore", for: .normal)
sender.tag = 0
}
}
6. If you are not using AutoLayoutConstraints, just change the Frame(Height) of UITextView on click of UIButton. No need of "textView_HeightConstraint"
#IBAction func showMore_Button_Clicked(_ sender: UIButton)
{
if sender.tag == 0
{
let height = self.getRowHeightFromText(strText: self.txtView.text)
self.txtView.frame = CGRect(x: self.txtView.frame.origin.x, y: self.txtView.frame.origin.y, width: self.txtView.frame.size.width, height: height)
btn_Show.setTitle("ShowLess", for: .normal)
sender.tag = 1
}
else
{
self.txtView.frame = CGRect(x: self.txtView.frame.origin.x, y: self.txtView.frame.origin.y, width: self.txtView.frame.size.width, height: 116)
btn_Show.setTitle("ShowMore", for: .normal)
sender.tag = 0
}
}

iOS - add image and text in title of Navigation bar

I would like to create a nav bar similar to what's in the image that's attached.
The title of the nav bar will be a combination of an image and text.
Should this be done per any best practice?
How can it be done?
As this answer shows, the easiest solution is to add the text to your image and add that image to the navigation bar like so:
var image = UIImage(named: "logo.png")
self.navigationItem.titleView = UIImageView(image: image)
But if you have to add text and an image separately (for example, in the case of localization), you can set your navigation bar's title view to contain both image and text by adding them to a UIView and setting the navigationItem's title view to that UIView, for example (assuming the navigation bar is part of a navigation controller):
// Only execute the code if there's a navigation controller
if self.navigationController == nil {
return
}
// Create a navView to add to the navigation bar
let navView = UIView()
// Create the label
let label = UILabel()
label.text = "Text"
label.sizeToFit()
label.center = navView.center
label.textAlignment = NSTextAlignment.Center
// Create the image view
let image = UIImageView()
image.image = UIImage(named: "Image.png")
// To maintain the image's aspect ratio:
let imageAspect = image.image!.size.width/image.image!.size.height
// Setting the image frame so that it's immediately before the text:
image.frame = CGRect(x: label.frame.origin.x-label.frame.size.height*imageAspect, y: label.frame.origin.y, width: label.frame.size.height*imageAspect, height: label.frame.size.height)
image.contentMode = UIViewContentMode.ScaleAspectFit
// Add both the label and image view to the navView
navView.addSubview(label)
navView.addSubview(image)
// Set the navigation bar's navigation item's titleView to the navView
self.navigationItem.titleView = navView
// Set the navView's frame to fit within the titleView
navView.sizeToFit()
Use horizontal UIStackView should be much cleaner and easier
Please add the next extension to UIViewController
extension UIViewController {
func setTitle(_ title: String, andImage image: UIImage) {
let titleLbl = UILabel()
titleLbl.text = title
titleLbl.textColor = UIColor.white
titleLbl.font = UIFont.systemFont(ofSize: 20.0, weight: .bold)
let imageView = UIImageView(image: image)
let titleView = UIStackView(arrangedSubviews: [imageView, titleLbl])
titleView.axis = .horizontal
titleView.spacing = 10.0
navigationItem.titleView = titleView
}
}
then use it inside your viewController:
setTitle("yourTitle", andImage: UIImage(named: "yourImage"))
(this will align the text and the icon together to the center, if you want the text to be centered and the icon in the left, just add an empty UIView with width constraint equal to the icon width)
here is my 2 cents for Swift 4, since accepted answer didn't work for me (was mostly off the screen):
// .. in ViewController
var navBar = CustomTitleView()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// =================== navBar =====================
navBar.loadWith(title: "Budget Overview", leftImage: Images.pie_chart)
self.navigationItem.titleView = navBar
}
class CustomTitleView: UIView
{
var title_label = CustomLabel()
var left_imageView = UIImageView()
override init(frame: CGRect){
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder){
super.init(coder: aDecoder)
setup()
}
func setup(){
self.addSubview(title_label)
self.addSubview(left_imageView)
}
func loadWith(title: String, leftImage: UIImage?)
{
//self.backgroundColor = .yellow
// =================== title_label ==================
//title_label.backgroundColor = .blue
title_label.text = title
title_label.font = UIFont.systemFont(ofSize: FontManager.fontSize + 5)
// =================== imageView ===================
left_imageView.image = leftImage
setupFrames()
}
func setupFrames()
{
let height: CGFloat = Navigation.topViewController()?.navigationController?.navigationBar.frame.height ?? 44
let image_size: CGFloat = height * 0.8
left_imageView.frame = CGRect(x: 0,
y: (height - image_size) / 2,
width: (left_imageView.image == nil) ? 0 : image_size,
height: image_size)
let titleWidth: CGFloat = title_label.intrinsicContentSize.width + 10
title_label.frame = CGRect(x: left_imageView.frame.maxX + 5,
y: 0,
width: titleWidth,
height: height)
contentWidth = Int(left_imageView.frame.width)
self.frame = CGRect(x: 0, y: 0, width: CGFloat(contentWidth), height: height)
}
var contentWidth: Int = 0 //if its CGFloat, it infinitely calls layoutSubviews(), changing franction of a width
override func layoutSubviews() {
super.layoutSubviews()
self.frame.size.width = CGFloat(contentWidth)
}
}
Swift 4.2 + Interface Builder Solution
As a follow-on to Lyndsey Scott's answer, you can also create a UIView .xib in Interface Builder, use that to lay out your title and image, and then update it on-the-fly via an #IBOutlet. This is useful for dynamic content, internationalization, maintainability etc.
Create a UIView subclass with a UILabel outlet and assign your new .xib to this class:
import UIKit
class FolderTitleView: UIView {
#IBOutlet weak var title : UILabel!
/// Create an instance of the class from its .xib
class func instanceFromNib() -> FolderTitleView {
return UINib(nibName: "FolderTitleView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! FolderTitleView
}
}
Connect the label to your outlet (title in my example) in your .xib, then in your UIViewController:
/// Reference to the title view
var folderTitleView : FolderTitleView?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Set the screen title to match the active folder
updateTitle()
}
/// Updates the title of the navigation controller.
func updateTitle() {
self.title = ""
if folderTitleView == nil {
folderTitleView = FolderTitleView.instanceFromNib()
self.navigationItem.titleView = folderTitleView
}
folderTitleView!.title.text = "Listening"
folderTitleView!.layoutIfNeeded()
}
This results in a nice self-centering title bar with an embedded image that you can easily update from code.
// worked for me
create a view and set the frame
now add the image in the view and set the frame
after adding the image, add the label in same view and set the frame
after adding the image and label to view, add same view to navigationItem
let navigationView = UIView(frame: CGRect(x: 0, y: 0, width: 50 , height: 55))
let labell : UILabel = UILabel(frame: CGRect(x: -38, y: 25, width: 150, height: 25))
labell.text = "Your text"
labell.textColor = UIColor.black
labell.font = UIFont.boldSystemFont(ofSize: 10)
navigationView.addSubview(labell)
let image : UIImage = UIImage(named: ValidationMessage.headerLogoName)!
let imageView = UIImageView(frame: CGRect(x: -20, y: 0, width: 100, height: 30))
imageView.contentMode = .scaleAspectFit
imageView.image = image
//navigationItem.titleView = imageView
navigationView.addSubview(imageView)
navigationItem.titleView = navigationView

Can you give UIStackView borders?

I'm trying to give 5 separate UIStackViews in my ViewController borders. I gave each of them an IBOutlet and then called them in viewDidLoad to use the layer property but to no avail. Is it not possible to give stack views borders, programatically?
Code:
#IBOutlet weak var stackView1: UIStackView!
#IBOutlet weak var stackView2: UIStackView!
#IBOutlet weak var stackView3: UIStackView!
#IBOutlet weak var stackView4: UIStackView!
#IBOutlet weak var stackView5: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
stackView1.layer.borderWidth = 5
stackView2.layer.borderWidth = 5
stackView3.layer.borderWidth = 5
stackView4.layer.borderWidth = 5
stackView5.layer.borderWidth = 5
}
Unfortunately this can't be done. UIStackView is unusual in that it is a "non-rendering" view which performs layout (using Auto Layout constraints) but does not display itself. It has a layer like all UIViews, but it's ignored.
See the Apple doc under "Managing the Stack View's Appearance":
The UIStackView is a nonrendering subclass of UIView. It does not
provide any user interface of its own. Instead, it just manages the
position and size of its arranged views. As a result, some properties
(like backgroundColor) have no affect on the stack view. Similarly,
you cannot override layerClass, drawRect:, or drawLayer:inContext:
Its possible to do this by having views inside the stack view be the borders. This can be a lot of work and there might be certain situations that either won't work or have to be worked around so it might not be worth the effort. You'll need to nest the stack views so you can provide borders in both the horizontal and vertical directions. In my Bordered Stack Views blog post I go into more detail about this. But basically I have regular views have a background set to the color of my choosing and I give height or width constraints of 1 depending on the direction of the stack view's axis. Here is the full hierarchy of a 2x2 grid built in interface builder:
Resulting in this result:
Here's a link to my github repo of this example so you can see the storyboard file.
You can embed stackView inside a UIView, then set borders of that view (color, width, etc), and then constraint stackView to that UIView like top, left, right, height.
Here's a handy chunk of code I found and use:
extension UIView {
func addTopBorderWithColor(color: UIColor, width: CGFloat) {
let border = CALayer()
border.backgroundColor = color.cgColor
border.frame = CGRect(x:0,y: 0, width:self.frame.size.width, height:width)
self.layer.addSublayer(border)
}
func addRightBorderWithColor(color: UIColor, width: CGFloat) {
let border = CALayer()
border.backgroundColor = color.cgColor
border.frame = CGRect(x: self.frame.size.width - width,y: 0, width:width, height:self.frame.size.height)
self.layer.addSublayer(border)
}
func addBottomBorderWithColor(color: UIColor, width: CGFloat) {
let border = CALayer()
border.backgroundColor = color.cgColor
border.frame = CGRect(x:0, y:self.frame.size.height - width, width:self.frame.size.width, height:width)
self.layer.addSublayer(border)
}
func addLeftBorderWithColor(color: UIColor, width: CGFloat) {
let border = CALayer()
border.backgroundColor = color.cgColor
border.frame = CGRect(x:0, y:0, width:width, height:self.frame.size.height)
self.layer.addSublayer(border)
}
func addMiddleBorderWithColor(color: UIColor, width: CGFloat) {
let border = CALayer()
border.backgroundColor = color.cgColor
border.frame = CGRect(x:self.frame.size.width/2, y:0, width:width, height:self.frame.size.height)
self.layer.addSublayer(border)
}
}
Simply use on any view like this:
bottomControls.addMiddleBorderWithColor(color: buttonBorderColor, width: 3.0)
Source: How to add only a TOP border on a UIButton?
As indicated by others you cannot do this (for details see the answer by Clafou).
What you can do, however, is embed your stack view in another UIView; making modifications to the layer of the enclosing UIView.
I think the easiest way to do it is by using no more labels or views with hight/width equals one to represent borders , I mean it is even easier than that via making use of SPACING attribute of stack views themselves . Just fill your stack and its substances , then make spacing one for outer vertical stack , also make spacing one for inner horizontal stacks , you get perfect result . Lastly for sake of giving a specific color to borders I maintained this using background view for the outer stckview , it just has same constraint like stack with background color as you wish to borders , idea is when you make spacing the spacing takes color of view behind the stack , that's it :D , kindly check results as in attached image and let me know if anything not clear
I have multiple UIStackViews inside a UIStackView.
I wanted a top and bottom border only for ONE of the UIStackViews in the stack so I added the UIStackView in question to a UIView with the background color set to the color of the top & bottom border color I wanted and replaced the bordered UIStackView in the arrangedSubviews with the UIView.
import UIKit
import Foundation
let goldBorderedUIView = UIView()
lazy var mainStackView: UIStackView =
{
let mainStack = UIStackView(arrangedSubviews: [goldBorderedUIView, stack2, stack3, stack 4])
mainStack.translatesAutoresizingMaskIntoConstraints = false
mainStack.axis = .vertical
mainStack.spacing = 0.5
mainStack.distribution = .fillEqually
return mainStack
}()
func setupBorderdStack() {
NSLayoutConstraint.activate([
borderedStackView.leadingAnchor.constraint(equalTo: goldBorderedUIView.leadingAnchor),
borderedStackView.topAnchor.constraint(equalTo: goldBorderedUIView.topAnchor, constant: 5),
borderedStackView.trailingAnchor.constraint(equalTo: goldBorderedUIView.trailingAnchor),
borderedStackView.bottomAnchor.constraint(equalTo: goldBorderedUIView.bottomAnchor, constant: -5)
])
}
override func viewDidLoad() {
super.viewDidLoad()
setupBorderdStack()
}
use like this
loseWeight.layer.borderColor = UIColor.orange.cgColor
loseWeight.layer.borderWidth = 1
The simplest way I've found to add a border to a UIStackView is to extend the stack view class and then add two layered views: the bottom one being the same size as the stack view, and the one on top that's used mask out the inside of the border, which is slightly smaller.
Here's the extension in Swift 5:
extension UIStackView {
func addBorder(color: UIColor, backgroundColor: UIColor, thickness: CGFloat) {
let insetView = UIView(frame: bounds)
insetView.backgroundColor = backgroundColor
insetView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
insertSubview(insetView, at: 0)
let borderBounds = CGRect(
x: thickness,
y: thickness,
width: frame.size.width - thickness * 2,
height: frame.size.height - thickness * 2)
let borderView = UIView(frame: borderBounds)
borderView.backgroundColor = color
borderView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
insertSubview(borderView, at: 0)
}
}
Then you add the border with a call like this:
myStackView.addBorder(color: .lightGray, backgroundColor: .white, thickness: 2)