Using a custom font in a UITextField causes it to shift slightly when accessed -- is there a fix? - interface-builder

I have a custom font in a UITextField, and I've noticed that when it's accessed (when the keyboard appears), the text shifts down by a very small amount -- maybe a pixel or two or three. (I've no way to measure it, of course, but it's enough for me to notice.) And then when the keyboard is dismissed, it shifts back up again.
I've tried clearing the field on editing (which hides the problem of the initial shift down), but it hasn't solved the problem. I've also looked over the various attributes in IB, and tried changing a few, but still the problem persists. I get the same results in the simulator and on my iPad2.
(The field is well clear of the keyboard, so it's not the entire view moving out of the way -- it's just the contents of that specific text field.)
I'm sure it's the custom font that's causing the problem -- it doesn't occur without it.
Any idea how to address this? I was thinking I might need to create the text field programmatically, instead of in IB -- and I know I probably ought to try that before asking the question here, but I'm loathe to go to all that trouble if it won't solve the problem.
Any advice appreciated!

I had this issue as well.
To fix, subclass UITextField and implement the following methods to adjust the positioning of text when not editing and editing.
- (CGRect)textRectForBounds:(CGRect)bounds {
return CGRectInset( bounds , 8 , 8 );
}
- (CGRect)editingRectForBounds:(CGRect)bounds {
return CGRectInset( bounds , 8 , 5 );
}

My solution is along the same lines a McDJ's, but with a slightly different twist. Subclass UITextField and override only these:
- (CGRect)placeholderRectForBounds:(CGRect)bounds {
return CGRectOffset( [self editingRectForBounds:bounds], 0, 2 );
}
- (CGRect)editingRectForBounds:(CGRect)bounds {
return CGRectOffset( [super editingRectForBounds:bounds], 0, -2 );
}
With the custom font I'm using, 2 points is the correct vertical adjustment, helping placeholder, "static", and "editing" text all stay on the same vertical line.

Unfortunately none of the answers worked for me.
#blackjacx answer worked but only sometimes :(
I started out debugging and here is what I've discovered:
1 - The real problem seems to be with a private subview of UITextField of type UIFieldEditorContentView
Below you can see that the y of it subview is not the same of the UITextField itself:
After realizing it I came out with the following workaround:
override func layoutSubviews() {
super.layoutSubviews()
fixMisplacedEditorContentView()
}
func fixMisplacedEditorContentView() {
if #available(iOS 10, *) {
for view in subviews {
if view.bounds.origin.y < 0 {
view.bounds.origin = CGPoint(x: view.bounds.origin.x, y: 0)
}
}
}
}
You will need to subclass UITextField and override layoutSubviews to add the ability to manually set to 0 the y of any subview that is set to a negative value. As this problem doesn't occur with iOS 9 our below I added a check to do the workaround only when it is on iOS 10.
The result you can see below:
2 - This workaround doesn't work if the user choose to select a subrange of the text (selectAll works fine)
Since the selection of the text is not a must have for my app I rather disable it. In order to do that you can use the following code (Swift 3):
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if #available(iOS 10, *) {
if action == #selector(UIResponderStandardEditActions.select(_:)) {
return false
}
}
return super.canPerformAction(action, withSender: sender)
}

Works for all font sizes and does not cause de-alignment with clearButton.
Subclass UITextField and override these as follows:
- (CGRect)placeholderRectForBounds:(CGRect)bounds
{
return CGRectOffset( bounds, 0, 4 );
}
- (CGRect)editingRectForBounds:(CGRect)bounds
{
return CGRectOffset( bounds, 0, 2);
}
- (CGRect)textRectForBounds:(CGRect)bounds
{
return CGRectOffset( bounds , 0 , 4 );
}

For a strange reason I didn't really understood I've solved this by setting automaticallyAdjustsScrollViewInsets to NO (or equivalent in Interface Builder). This with iOS 8.1.

I had this issue with a custom font and solved it by shifting the label in the other direction when the keyboard events would fire. I moved the center of the label in the button by overriding the drawRect: method
- (void)drawRect:(CGRect)rect
{
self.titleLabel.center = CGPointMake(self.titleLabel.center.x, self.titleLabel.center.y+3);
}

This is expected behaviour in a standard UITextField. You can however solve this by subclassing UITextField and by adjusting the bounds for the text itself.
Swift 3
override func textRect(forBounds bounds: CGRect) -> CGRect {
return(bounds.insetBy(dx: 0, dy: 0))
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
return(bounds.insetBy(dx: 0, dy: -0.5))
}
override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
return(bounds.insetBy(dx: 0, dy: 0))
}
This should do the trick!

Set your textfields Border Style to any value except "none" in IB, then, in your ViewController's viewDidLoad set:
yourTextField.borderStyle = .none
(Based on this answer by Box Jeon)

Swift 3
Do not forget the accessory views of the UITextField. You'll need to account for super of the *rect(forBounds: ...) functions if you want a working implementation. And be also sure to only displace the rects for the buggy iOS 10 and not for 9 or 8! The following code should do the trick:
public class CustomTextField: UITextField {
public override func textRect(forBounds bounds: CGRect) -> CGRect {
let superValue = super.textRect(forBounds: bounds)
if #available(iOS 10, *) {
return superValue.insetBy(dx: 0, dy: 0)
}
return superValue
}
public override func editingRect(forBounds bounds: CGRect) -> CGRect {
let superValue = super.editingRect(forBounds: bounds)
if #available(iOS 10, *) {
return superValue.insetBy(dx: 0, dy: -0.5)
}
return superValue
}
public override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
let superValue = super.placeholderRect(forBounds: bounds)
if #available(iOS 10, *) {
if isEditing {
return superValue.insetBy(dx: 0, dy: 0.5)
}
return superValue.insetBy(dx: 0, dy: 0.0)
}
return superValue
}
}
EDIT
I slightly edited my code from above to the following and it works better for me. I testet it on iPhone 6, 6s, 7, 7s as well as the 'plus' devices with iOS 9.3 and 10.3.
public class CustomTextField: UITextField {
public override func textRect(forBounds bounds: CGRect) -> CGRect {
let superValue = super.textRect(forBounds: bounds)
if #available(iOS 10, *) {
return superValue.insetBy(dx: 0, dy: -0.3)
}
return superValue.insetBy(dx: 0, dy: -0.2)
}
public override func editingRect(forBounds bounds: CGRect) -> CGRect {
return self.textRect(forBounds: bounds)
}
}
I think it also depends on the font you use. I use UIFont.systemFont(ofSize: 17.0, weight: UIFontWeightLight)

You should set Font property earlier than Text property.

Related

Why is NSComboBox content in the incorrect position?

I'm trying to layout an NSComboBox in a Mac app and I'm getting some weird behaviour. In reality I've got a view which has an NSComboBox as a subview along with some other subviews, but I've created a simple example to display the same issue I'm seeing.
For some reason, the text field isn't exactly vertically centered like I'd expect.
Here's my TestComboBox:
Here's an Apple NSComboBox (note the text is vertically centered and there's a small amount of leading spacing before the highlight colour):
I've created a simple example to show the issue:
class TestComboBox: NSView {
private let comboBox = NSComboBox(labelWithString: "")
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
comboBox.isEditable = true
addSubview(comboBox)
}
override func layout() {
comboBox.sizeToFit()
comboBox.frame = CGRect(x: 0, y: 0, width: bounds.width, height: comboBox.bounds.height)
}
}
If you add the above view to a basic Mac app (Storyboard) and pin it to the view controller with the following constraints:
I think I'm trying to layout the combo box correctly, but I'm not sure why it looks slightly different to the Apple example as they're both using NSComboBox!
Any guidance much appreciated!
The combo box is initialized with
private let comboBox = NSComboBox(labelWithString: "")
but init(labelWithString:) is a convenience initializer of NSTextField to create a label. Use init() instead:
private let comboBox = NSComboBox()

NSPopover Background Color (Including Triangle) with macOS Mojave Dark Mode

Swift 4.2, Xcode 10, macOS 10.14
I have created the following NSView subclass that I put on the root view of all my NSPopover instances in my storyboard. But I noticed that when I switch the color mode in macOS Mojave (from dark to light or the other way around) that it doesn't update the background color of my NSPopover.
class PopoverMain:NSView{
override func viewDidMoveToWindow() {
guard let frameView = window?.contentView?.superview else { return }
let backgroundView = NSView(frame: frameView.bounds)
backgroundView.backgroundColor(color: Color(named: "MyColor")!)
backgroundView.autoresizingMask = [.width, .height]
frameView.addSubview(backgroundView, positioned: .below, relativeTo: frameView)
}
}
I believe this is because a color mode transition only calls these methods (source) and not viewDidMoveToWindow():
updateLayer()
draw(_:)
layout()
updateConstraints()
Has anyone figured out a reliable way to color the background of an NSPopover (including its triangle) and have it work seamlessly on macOS Mojave?
It's funny how writing up your question leads to a solution (sometimes quickly). I realized I needed to create another NSView subclass responsible for generating the NSView that's loaded into the NSPopover. Note the addition of the PopoverMainView class:
class PopoverMain:NSView{
override func viewDidMoveToWindow() {
guard let frameView = window?.contentView?.superview else { return }
let backgroundView = PopoverMainView(frame: frameView.bounds)
backgroundView.autoresizingMask = [.width, .height]
frameView.addSubview(backgroundView, positioned: .below, relativeTo: frameView)
}
}
class PopoverMainView:NSView {
override func draw(_ dirtyRect: NSRect) {
Color(named: "MyColor")!.set()
self.bounds.fill()
}
}

How can I implement a tab bar like the ones in Xcode?

There does not seem to be any standard AppKit control for creating a tab bar similar to the ones found in Xcode.
Any idea on whether this is possible or would I need to use a custom control of some sort, and if so any suggestions on whether any are available.
OK I figured it out by subclassing NSSegementedControl to get the desired behaviours.
import Cocoa
class OSSegmentedCell: NSSegmentedCell {
override func draw(withFrame cellFrame: NSRect, in controlView: NSView) {
// Do not call super to prevent the border from being draw
//super.draw(withFrame: cellFrame, in: controlView)
// Call this to ensure the overridden drawSegment() method gets called for each segment
super.drawInterior(withFrame: cellFrame, in: controlView)
}
override func drawSegment(_ segment: Int, inFrame frame: NSRect, with controlView: NSView) {
// Resize the view to leave a small gap
let d:CGFloat = 0.0
let width = frame.height - 2.0*d
let dx = (frame.width - width) / 2.0
let newRect = NSRect(x: frame.minX+dx, y: frame.minY, width: width, height: width)
if let image = self.image(forSegment: segment) {
if self.selectedSegment == segment {
let tintedImage = image.tintedImageWithColor(color: NSColor.blue)
tintedImage.draw(in: newRect)
} else {
image.draw(in: newRect)
}
}
}
}
extension NSImage {
func tintedImageWithColor(color:NSColor) -> NSImage {
let size = self.size
let imageBounds = NSMakeRect(0, 0, size.width, size.height)
let copiedImage = self.copy() as! NSImage
copiedImage.lockFocus()
color.set()
__NSRectFillUsingOperation(imageBounds, NSCompositingOperation.sourceIn)
copiedImage.unlockFocus()
return copiedImage
}
}

Handle NSButton click programmatically

I'm attempting to make a subclassed NSButton handle a click event with no luck.
To this point I have been customizing the drawRect functions in my views to get custom looks and drawing these views programmatically. To this end, I have essentially orphaned my usage of the Interface builder (simply to avoid its sheer complexity).
Is there a way for me to assign a click (or mouseDown in this scenario) handler programmatically, this doesn't seem to be working
class MainButton: NSButton {
override func mouseDown(theEvent: NSEvent!) {
let alert = NSAlert()
alert.messageText = "Title"
alert.runModal()
}
override func drawRect(rect: NSRect) {
super.drawRect(rect)
let circle : NSBezierPath = NSBezierPath(
roundedRect: rect, xRadius: rect.size.width / 2,
yRadius: rect.size.height / 2
)
circle.lineWidth = 2
NSColor(calibratedWhite: 0.2, alpha: 1).setStroke()
circle.stroke()
}
}
Here is a full gist of the app for context if needed.

iOS 8 - UIPopoverPresentationController moving popover

I am looking for an effective way to re-position a popover using the new uipopoverpresentationcontroller. I have succesfully presented the popover, and now I want to move it without dismissing and presenting again. I am having trouble using the function:
(void)popoverPresentationController:(UIPopoverPresentationController *)popoverPresentationController
willRepositionPopoverToRect:(inout CGRect *)rect
inView:(inout UIView **)view
I know it's early in the game, but it anyone has an example of how to do this efficiently I would be grateful if you shared it with me. Thanks in advance.
Unfortunately this hacky workaround is the only solution I've found:
[vc.popoverPresentationController setSourceRect:newSourceRect];
[vc setPreferredContentSize:CGRectInset(vc.view.frame, -0.01, 0.0).size];
This temporarily changes the content size of the presented view, causing the popover and arrow to be repositioned. The temporary change in size is not visible.
It seems this is a problem Apple need to fix - changing the sourceView or sourceRect properties of UIPopoverPresentationController does nothing when it's already presenting a popover (without this workaround).
Hope this works for you too!
I had luck using containerView?.setNeedsLayout() and containerView?.layoutIfNeeded() after changing the sourceRect of the popoverPresentationController, like so:
func movePopoverTo(_ newRect: CGRect) {
let popover = self.presentedViewController as? MyPopoverViewController {
popover.popoverPresentationController?.sourceRect = newRect
popover.popoverPresentationController?.containerView?.setNeedsLayout()
popover.popoverPresentationController?.containerView?.layoutIfNeeded()
}
}
And even to have a popover follow a tableView cell without having to change anything:
class MyTableViewController: UITableViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "MyPopoverSegue" {
guard let controller = segue.destination as? MyPopoverViewController else { fatalError("Expected destination controller to be a 'MyPopoverViewController'!") }
guard let popoverPresentationController = controller.popoverPresentationController else { fatalError("No popoverPresentationController!") }
guard let rowIndexPath = sender as? IndexPath else { fatalError("Expected sender to be an 'IndexPath'!") }
guard myData.count > rowIndexPath.row else { fatalError("Index (\(rowIndexPath.row)) Out Of Bounds for array (count: \(myData.count))!") }
if self.presentedViewController is MyPopoverViewController {
self.presentedViewController?.dismiss(animated: false)
}
popoverPresentationController.sourceView = self.tableView
popoverPresentationController.sourceRect = self.tableView.rectForRow(at: rowIndexPath)
popoverPresentationController.passthroughViews = [self.tableView]
controller.configure(myData[rowIndexPath.row])
}
super.prepare(for: segue, sender: sender)
}
}
// MARK: - UIScrollViewDelegate
extension MyTableViewController {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let popover = self.presentedViewController as? MyPopoverViewController {
popover.popoverPresentationController?.containerView?.setNeedsLayout()
popover.popoverPresentationController?.containerView?.layoutIfNeeded()
}
}
}
I used the same method as mentioned in another answer by #Rowan_Jones, however I didn't want the popover's size to actually change. Even by fractions of a point. I realized that you can set the preferredContentSize multiple times back to back, but visually it's size will only change to match the last value.
[vc.popoverPresentationController setSourceRect:newSourceRect];
CGSize finalDesiredSize = CGSizeMake(320, 480);
CGSize tempSize = CGSizeMake(finalDesiredSize.width, finalDesiredSize.height + 1);
[vc setPreferredContentSize:tempSize];
[vc setPreferredContentSize:finalDesiredSize];
So even if finalDesiredSize is the same as your initial preferredContentSize this will cause the popover to be updated, even though it's size doesn't actually change.
Here is an example for how to recenter the popover:
- (void)popoverPresentationController:(UIPopoverPresentationController *)popoverPresentationController willRepositionPopoverToRect:(inout CGRect *)rect inView:(inout UIView **)view {
*rect = CGRectMake((CGRectGetWidth((*view).bounds)-2)*0.5f,(CGRectGetHeight((*view).bounds)-2)*0.5f, 2, 2);
I have also used this method to ensure that the popover moved to the correct location after moving by setting the *rect and the *view to the original sourceRect and sourceView.
As an additional note, I don't believe that this method is called when the popover's source is set using a bar button item.
I'm posting this because I don't have enough points to vote or comment. :)
#turbs's answer worked for me perfectly. It should be the accepted answer.
Setting *rect to the rect you need in the delegate method:
(void)popoverPresentationController:(UIPopoverPresentationController *)popoverPresentationController
willRepositionPopoverToRect:(inout CGRect *)rect
inView:(inout UIView **)view
iOS 12.3
[vc.popoverPresentationController setSourceRect:newSourceRect];
[vc.popoverPresentationController.containerView setNeedsLayout];

Resources