Swift 4: How to change font attributes of number in multiple UITextFields when .count > 3 and how to reverse calculation? - uitextfield

I have three UITextFields that will only contain a whole number between 0 and either 13066 or 3915 maximum. It's presented with a special font where font1 at 50pt is larger than font2 at 50pt so I only need to play around with the font file, not the size.
1) I need help finding a way to have the hundreds presented with font2 but when the number >= 1000, the digits for thousands is presented with font1, while the hundreds still maintain font2. As this is a UITextField input, I need this to happen real-time.
Animated picture of how the end result eventually will be!
2) If you watch the animation, you'll see that each of the three fields totals to the field at the bottom. This is straight forward. However, I also want to reverse this, i.e filling in the total and the dials and digits should fill in as shown (half in each MAIN up to 3920 and remaining in CTR up to 13066). How do I code a functioning reversible calculation like that and avoid conflict?
This is what I got so far but it doesn't quite do what I want yet:
`class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var centerField: UITextField!
#IBOutlet weak var main1Field: UITextField!
#IBOutlet weak var main2Field: UITextField!
#IBOutlet weak var totalField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
//UITextField delegation
centerField.delegate = self
main1Field.delegate = self
main2Field.delegate = self
totalField.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
//Change font when number input exceeds 3 digits
func getAttributedString(for number: Int) -> NSAttributedString {
let defaultAttributes = [
NSAttributedStringKey.font: UIFont(name: "PMDG_777_DU_A", size: UIFont.labelFontSize)!
]
let bigNumberAttributes = [
NSAttributedStringKey.font: UIFont(name: "PMDG_777_DU_B", size: UIFont.labelFontSize)!
]
let attributedString = NSMutableAttributedString(string: "\(number)", attributes: defaultAttributes)
if attributedString.length > 3 {
let substr = attributedString.string.dropLast(3)
let range = NSMakeRange(0, substr.utf16.count)
attributedString.setAttributes(bigNumberAttributes, range: range)
}
return attributedString
}
//Hide keyboard when hitting Return
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
//Hide keyboard when tapping outside of field
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
//Real-time calculation of entries made to the fields and output it live to the total
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//Convert the String inserted into the fields into Int and create variables
let centerFuel = Int(centerField.text!) ?? 0
centerField.attributedText = getAttributedString(for: centerFuel)
let main1Fuel = Int(main1Field.text!) ?? 0
main1Field.attributedText = getAttributedString(for: main1Fuel)
let main2Fuel = Int(main2Field.text!) ?? 0
main2Field.attributedText = getAttributedString(for: main2Fuel)
let total: Int = centerFuel + main1Fuel + main2Fuel
totalField.attributedText = getAttributedString(for: total)
return true
}

Use NSMutableAttributedString:
func getAttributedString(for number: Int) -> NSAttributedString {
precondition(number >= 0)
let defaultAttributes = [
NSAttributedStringKey.font: UIFont.systemFont(ofSize: 18)
]
let bigNumberAttributes = [
NSAttributedStringKey.font: UIFont.systemFont(ofSize: 30)
]
let attributedString = NSMutableAttributedString(string: "\(number)", attributes: defaultAttributes)
if attributedString.length > 3 {
let range = NSMakeRange(0, attributedString.length - 3)
attributedString.setAttributes(bigNumberAttributes, range: range)
}
return attributedString
}
Then set the attributedText property on your label:
label.attributedText = getAttributedString(for: 13006)
And you can get result like this:
Adjust the styles to taste!

Here is a sample solution:
func attributedString(fromIntStr: String) -> NSAttributedString {
print("Working with: \(fromIntStr)")
let finalAttr = NSMutableAttributedString.init()
let length = fromIntStr.count
if length > 3 //we test this because we can't do dropLast(n) where n is negative
{
let start = String(fromIntStr.dropLast(3))
let firstPart = NSAttributedString.init(string: start,
attributes: [.font: UIFont.systemFont(ofSize: 20)])
finalAttr.append(firstPart)
}
var dropEnd = 0
if length > 3 //we test this because we can't do dropFirst(n) where n is negative
{
dropEnd = length - 3
}
else
{
dropEnd = 0 //That's just an explicit value but it was already set by default
}
let end = String(fromIntStr.dropFirst(dropEnd))
let lastPart = NSAttributedString.init(string: end,
attributes: [.font: UIFont.systemFont(ofSize: 15)])
finalAttr.append(lastPart)
//It seems that you want a right alignement and since we are using NSAttributedString we can do it by using ParagraphStyle
let paragraphStyle = NSMutableParagraphStyle.init()
paragraphStyle.alignment = .right
finalAttr.addAttribute(.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: finalAttr.length))
return finalAttr
}
It can be tested on Playground adding this at the end:
let values = ["", "9", "89", "789", "6789", "56789"]
let view = UIView.init(frame: CGRect(x: 0, y: 0, width: 800, height: 400))
for (i, str) in values.enumerated().reversed()
{
let label = UILabel.init(frame: CGRect(x: 0, y: i*50, width: 800, height: 50))
let attr = attributedString(fromIntStr: str)
label.attributedText = attr
view.addSubview(label)
}
view
There are various way to do this but here is the logic I used:
• Create a NSMutableAttributedString.
• Separate the string in two strings (one for 0-999, the other one for the rest)
• Create from them two NSAttributedString with the corresponding font
• Append them to the NSMutableAttributedString previously created.
Of course this could be improved:
Is dropFirst()/dropLast() the best solutions?
I supposed that you add already a String value from a Int (I didn't do the conversion).
Another solution more classic would have been:
• Create a NSMutableAttributedString from the whole String.
• Apply small font on range last from last-3
• Apply big font on range start to last -3
Last point be ommited if you apply the big font from the start on the whole string.

Related

SwiftUI text field orphan on macOS

I have a text field like this
Text("Hello, one two three four five six seven eight!")
.frame(width:270)
.border(.blue)
When it renders it decides to put seven and eight on the second line even though there is space for seven on the first line. Worse it decides to indent the truncated top line so it is centred within the frame.
How do I fix this so it wraps the text properly without taking into account the orphan?
Edit: Forgot to mention that I wanted this on macOS. I have tried to port it to the Mac. It does correctly left align the text but it doesn't wrap to the second line. The height of the box does get calculated accordingly though.
Here is my updated code:
struct NonOrphanedText: View
{
var text: String
#State private var height: CGFloat = .zero
var body: some View
{
InternalLabelView(text: text, dynamicHeight: $height)
.frame(maxHeight: height)
}
struct InternalLabelView: NSViewRepresentable
{
var text: String
#Binding var dynamicHeight: CGFloat
func makeNSView(context: Context) -> NSTextField
{
let label = NSTextField()
label.isEditable = false
label.isBezeled = false
label.drawsBackground = false
label.isSelectable = false
label.maximumNumberOfLines = 5
label.usesSingleLineMode = false
label.lineBreakStrategy = .init()
label.lineBreakMode = .byWordWrapping
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}
func updateNSView(_ nsView: NSTextField, context: Context)
{
nsView.stringValue = text
DispatchQueue.main.async
{
dynamicHeight = nsView.sizeThatFits(CGSize(width: nsView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
}
}
}
}
We need lineBreakStrategy but it is unavailable for now in SwiftUI, so possible solution is to use UILabel.
Here is a possible solution. Tested with Xcode 13.2 / iOS 15.2
struct LabelView: View {
var text: String
#State private var height: CGFloat = .zero
var body: some View {
InternalLabelView(text: text, dynamicHeight: $height)
.frame(maxHeight: height)
}
struct InternalLabelView: UIViewRepresentable {
var text: String
#Binding var dynamicHeight: CGFloat
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakStrategy = .init() // << here !!
label.lineBreakMode = .byWordWrapping
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
DispatchQueue.main.async {
dynamicHeight = uiView.sizeThatFits(CGSize(width: uiView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
}
}
}
}

Difficulty removing UITextView border while keeping indentation

I'm writing an iOS app where the user can add text fields, then drag them around the screen to reposition them, layout-style, sort of like Keynote.
I'm currently appending the user-added UITextFields to an #IBOutlet Collection and defaulting to .borderStyle = .roundedRect to get a faint border around the selected text, indicating the field is selected. Any UITextField will be set to .roundedRect border style when textFieldDidBeginEditing is called, and switch to textField.borderStyle = .none when textFieldDidEndEditing is called.
All seems to work with one problem: when switching border style to .none, the text field loses indentation that was around the border, shifting text outward and putting it in a spot where the user hadn't intended (graphic adds a background color red, just to show the shift, but I'll eventually allow the user to set background colors, so just shifting the UITextField isn't an option).
I've also tried adapting the answer at:
Create space at the beginning of a UITextField
setting a no-padding inset for the TextView when it's a .roundedRect, but adding padding when .borderStyle is .none. This seems to have no effect.
Other answers have suggested setting
textField.layer.borderColor = UIColor.clear.cgColor
or
textField.layer.borderWidth = 0.0
but these don't seem to have any effect, either
I'm eventually going to allow the user to change fonts & sizes of each TextField, so I'd like any indentation to be consistent whether the UITextField is selected or nots elected, and regardless of font choices.
Code is below. Recommendations are most welcome, as well as setting me on a new approach, if I'm missing a better solution.
Thanks!
John
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var screenView: UIView! // a 320 x 240 view
#IBOutlet var fieldCollection: [UITextField]! // Not connected, fields created programmatically
// below are used in .inset(by:) but seems to have no effect
let padding = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)
let noPadding = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
override func viewDidLoad() {
super.viewDidLoad()
// hide keyboard if we tap outside of a field
let tap = UITapGestureRecognizer(target: self.view, action: #selector(UIView.endEditing(_:)))
tap.cancelsTouchesInView = false
self.view.addGestureRecognizer(tap)
createNewField()
}
// Select / deselect text fields
func textFieldDidBeginEditing(_ textField: UITextField) {
textField.borderStyle = .roundedRect
// textField.bounds.inset(by: noPadding) // effect is the same if left out
}
func textFieldDidEndEditing(_ textField: UITextField) {
textField.borderStyle = .none
// textField.bounds.inset(by: padding) // effect is the same if left out
}
// UITextField created & added to fieldCollection
func createNewField() {
let newFieldRect = CGRect(x: 0, y: 0, width: 320, height: 30)
let newField = UITextField(frame: newFieldRect)
newField.borderStyle = .roundedRect
newField.isUserInteractionEnabled = true
newField.addGestureRecognizer(addGestureToField())
screenView.addSubview(newField)
if fieldCollection == nil {
fieldCollection = [newField]
} else {
fieldCollection.append(newField)
}
newField.delegate = self
newField.becomeFirstResponder()
}
func addGestureToField() -> UIPanGestureRecognizer {
var panGesture = UIPanGestureRecognizer()
panGesture = UIPanGestureRecognizer(target: self, action: #selector(draggedView(_:)))
return panGesture
}
// event handler when a field(view) is dragged
#objc func draggedView(_ sender:UIPanGestureRecognizer){
sender.view!.becomeFirstResponder()
let selectedView = sender.view as! UITextField
selectedView.bringSubviewToFront(selectedView)
let translation = sender.translation(in: screenView)
selectedView.center = CGPoint(x: selectedView.center.x + translation.x, y: selectedView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: screenView)
}
#IBAction func addFieldPressed(_ sender: UIButton) {
createNewField()
}
}
I was able to work around the problem by subclassing UITextField:
class PaddedTextField: UITextField {
let padding = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
let noPadding = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
override open func textRect(forBounds bounds: CGRect) -> CGRect {
if self.borderStyle == .none {
let content = bounds.inset(by: padding)
return content
} else {
return bounds.inset(by: noPadding)
}
}
}
I then changed the newField object creation from using UITextField to:
let newField = PaddedTextField(frame: newFieldRect)
One more change. The height needed to be more appropriately calculated. Since all of my text fields can start out the full length of the enclosing superview (320 points), I modified the original newFieldRect, used .sizeToFit() to create a textbox with the appropriate height. The other dimensions won't be correct b/c I don't have anything in the text view, but I extract the .height and reuse this with my original initliazation parameters.
newField.sizeToFit()
let newFieldHeight = newField.frame.height
newFieldRect = CGRect(x: 0, y: 0, width: 320, height: newFieldHeight)
newField.frame = newFieldRect
Here's hoping it helps save someone time.

How to programmatically create several NSTextFields?

I have an array of CGPoints. I need a personal label for every fourth point in array, so I need to create several NSTextFields programmatically. I can add points with mouse clicks and can create as many points as I wish. Labels for these points must be all active to show text for user simultaneously. How can I do it?
(macOS, Xcode 7, Swift 2)
Here's my code:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
#IBOutlet weak var drawView: DrawView!
#IBOutlet weak var view: NSView!
let label = NSTextField(frame: NSMakeRect(0,0,100,50))
var pointsArray: [CGPoint] = []
func applicationWillUpdate(aNotification: NSNotification) {
label.backgroundColor = NSColor.clearColor()
label.bezeled = false
label.stringValue = "\(pointsArray.count/4)"
var multiple = (1...25).map { _ in label }
for index in 0..<(pointsArray.count/4) {
let point = CGPoint(x: pointsArray[index*4].x, y: pointsArray[index*4].y)
label.frame = CGRect(origin: point, size: CGSize(width: label.bounds.width, height: label.bounds.height))
let sticker = multiple[index]
view.addSubview(sticker)
}
}
}
At runtime I see only one label but I need to see several labels simultaneously (on every fourth CGPoint). If I have 100 CGPoints I must have 25 labels.
I see only one label
Now that I've straightened out your curly braces and indentation, it's easy to see why. Your loop is incorrectly constructed, so that you create one label and change its frame four times. You need to create four separate labels with four separate frames.
Code for creating several NSTextFields:
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var drawView: DrawView!
var pointsArray: [CGPoint] = []
var label1 = NSTextField(frame: NSMakeRect(0,0,100,50))
var label2 = NSTextField(frame: NSMakeRect(0,0,100,50))
var label3 = NSTextField(frame: NSMakeRect(0,0,100,50))
// ....................................................
var label25 = NSTextField(frame: NSMakeRect(0,0,100,50))
override func awakeFromNib() {
super.awakeFromNib()
var labelArray = [label1, label2, label3, ....., label25]
for i in 0 ..< (pointsArray.count / 4) {
labelArray[i].backgroundColor = NSColor.clearColor()
labelArray[i].bezeled = false
labelArray[i].stringValue = "\(i + 1)"
let point = CGPoint(x: (pointsArray[i * 4].x),
y: (pointsArray[i * 4].y))
var originPoint: [CGPoint] = []
originPoint.append(point)
labelArray[i].frame = .init(origin: originPoint[0],
size: CGSize(width: labelArray[i].bounds.width,
height: labelArray[i].bounds.height))
self.view.addSubview(labelArray[i])
}
}
}

NSDocument printOperationWithSettings not showing all pages

In NSDocument subclass, have this function:
override func printOperationWithSettings(printSettings: [String : AnyObject]) throws -> NSPrintOperation {
let printInfo: NSPrintInfo = self.printInfo
var pageSize = printInfo.paperSize
pageSize.width -= printInfo.leftMargin + printInfo.rightMargin
pageSize.height -= printInfo.topMargin + printInfo.bottomMargin
pageSize.width = pageSize.width * 2
pageSize.height = pageSize.height * 2
let myPage = MyPage(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: pageSize))
let printOperation = NSPrintOperation(view: myPage, printInfo: printInfo)
return printOperation
}
MyPage is, for this test, an NSView subclass that just draws an oval.
class MyPage: NSView {
override var flipped: Bool {
return true
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
NSColor.greenColor().set() // choose color
let figure = NSBezierPath() // container for line(s)
figure.appendBezierPathWithOvalInRect(self.frame)
figure.stroke() // draw line(s)
}
}
I'd expect this to show four pages in the print panel, but it only shows two, equating to the top left and bottom left of the oval. No matter how wide I make myPage's frame, only the leftmost pages are shown. Any ideas why? Thank you!

Dynamically adding cells to a NSMatrix laid out with Auto Layout has weird effects; why?

I want to create a group of radio buttons using the NSMatrix method that Interface Builder uses, but in code. The matrix is laid out using Auto Layout. I have it mostly working, except for when I add new options at runtime.
In the following example, clicking Append Item a few times will work fine, then the matrix starts going out of the window near the top (at least I think it's clipped at the top). If you maximize this window after adding a bunch of items, the window will stay the same height and all the items will be clipped to about a pixel high each, which is a very undesirable thing :)
In my real program (not this test below), it works mostly fine, but if I add an option dynamically, after certain numbers of items (initially 5), the options will clip very slightly, appearing slightly squeezed or squished. Adding another option reverts this until the next magic number is hit.
What's going on? I'm testing this on OS X Yosemite. Thanks.
// 17 august 2015
import Cocoa
var keepAliveMainwin: NSWindow? = nil
var matrix: NSMatrix? = nil
class ButtonHandler : NSObject {
#IBAction func onClicked(sender: AnyObject) {
var lastRow = matrix!.numberOfRows
matrix!.renewRows(lastRow + 1, columns: 1)
var cell = matrix!.cellAtRow(lastRow, column: 0) as! NSButtonCell
cell.title = "New Item"
matrix!.sizeToCells()
}
}
var buttonHandler: ButtonHandler = ButtonHandler()
func appLaunched() {
var mainwin = NSWindow(
contentRect: NSMakeRect(0, 0, 320, 240),
styleMask: (NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask),
backing: NSBackingStoreType.Buffered,
defer: true)
var contentView = mainwin.contentView as! NSView
var prototype = NSButtonCell()
prototype.setButtonType(NSButtonType.RadioButton)
prototype.font = NSFont.systemFontOfSize(NSFont.systemFontSizeForControlSize(NSControlSize.RegularControlSize))
matrix = NSMatrix(frame: NSZeroRect,
mode: NSMatrixMode.RadioModeMatrix,
prototype: prototype,
numberOfRows: 0,
numberOfColumns: 0)
matrix!.allowsEmptySelection = false
matrix!.selectionByRect = true
matrix!.intercellSpacing = NSMakeSize(4, 2)
matrix!.autorecalculatesCellSize = true
matrix!.drawsBackground = false
matrix!.drawsCellBackground = false
matrix!.autosizesCells = true
matrix!.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(matrix!)
var button = NSButton(frame: NSZeroRect)
button.title = "Append Item"
button.setButtonType(NSButtonType.MomentaryPushInButton)
button.bordered = true
button.bezelStyle = NSBezelStyle.RoundedBezelStyle
button.font = NSFont.systemFontOfSize(NSFont.systemFontSizeForControlSize(NSControlSize.RegularControlSize))
button.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(button)
button.target = buttonHandler
button.action = "onClicked:"
var views: [String: NSView]
views = [
"button": button,
"matrix": matrix!,
]
addConstraints(contentView, "V:|-[matrix]-[button]-|", views)
addConstraints(contentView, "H:|-[matrix]-|", views)
addConstraints(contentView, "H:|-[button]-|", views)
mainwin.cascadeTopLeftFromPoint(NSMakePoint(20, 20))
mainwin.makeKeyAndOrderFront(mainwin)
keepAliveMainwin = mainwin
}
func addConstraints(view: NSView, constraint: String, views: [String: NSView]) {
var constraints = NSLayoutConstraint.constraintsWithVisualFormat(
constraint,
options: NSLayoutFormatOptions(0),
metrics: nil,
views: views)
view.addConstraints(constraints)
}
class appDelegate : NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(note: NSNotification) {
appLaunched()
}
func applicationShouldTerminateAfterLastWindowClosed(app: NSApplication) -> Bool {
return true
}
}
func main() {
var app = NSApplication.sharedApplication()
app.setActivationPolicy(NSApplicationActivationPolicy.Regular)
// NSApplication.delegate is weak; if we don't use the temporary variable, the delegate will die before it's used
var delegate = appDelegate()
app.delegate = delegate
app.run()
}
main()
Apparently, you need to omit the call to sizeToCells() after calling renewRows(_:columns:). My guess is that it sets the frame size, which is mostly useless when using auto layout, but also clears a "dirty" flag somewhere that tells the matrix that it needs to invalidate its intrinsic size. In other words, the matrix thinks it already did the re-layout stuff it needed to do.

Resources