5

I'm creating a natural language form and whenever the user enters an input which is quite large, I want the entire line to move to the next line (just like an UITextView). Right now, I get this result: http://cl.ly/image/1E1n0W28360T

This indicates two obvious problems: for one: the element the UITextField is pushing should go to the next line, and secondly, when spacing back, the element that was 'pushed away' does not get 'pushed back' into place. Also, the UITextField should move to the next line when exiting the view.bounds. It is arbitrary whether it's best to use a UITextField or UITextView for. It should be applicable to a situation in picture 2.

This is a more graphical approach to what I'm trying to achieve: enter image description here

How do I solve this? And is this the right approach?

Update

The answer of Robert is very good one, next to some bugs that it still has there are is also the issue that it's not flexible. I've started refactoring the code and tried to subclass a UITextField and a UITextView, following the approach of Robert.

When subclassing the code there needs to be some delegation by the UITextField to the UITextView. Secondly, every part of the sentence needs to be split whenever there's a UITextField in between, but I feel like that can be hard coded into the VC as well. The constraints need to be converted to code as well.

Whenever I've got a solution to either one of all those problems I'll update the question and hopefully get to a flexible solution :)

bdv
  • 1,154
  • 2
  • 19
  • 42
  • Please accept my answer if it was helpful. You will probably decide for yourself if my answer complies with the bounty's requirements. – Robert Nov 27 '14 at 17:09
  • Sure, I am still looking for a more flexible answer though, but your solution deserves a checkmark for now :) – bdv Nov 28 '14 at 01:06

1 Answers1

3

Your approach works for me.

Let's say you have a UITextView that displays the selectable, but non-editable full sentence, including the entered parameter values. And then you have an editable UITextField for each form parameter. With this setup you can leave it to the UITextView to handle the text flow and use the UITextViews to handle the input.

In order to let the UITextField appear within the text flow, the trick is to hide it – or rather reduce it to the width of its caret – and display it at the position of last character of the parameter's value's.

@interface ViewController ()

@property (strong, nonatomic) IBOutlet UITextView *fullTextView;
@property (strong, nonatomic) IBOutlet UITextField *friendField;

// Using AutoLayout constraints to position the friendField
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *friendFieldLeadingConstraint;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *friendFieldTopConstraint;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *friendFieldWidthConstraint;

@property (assign, nonatomic) CGFloat initialFriendFieldWidth;

@end

@implementation ViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  // Store the intrinsic size of the friendField displaying the placeholder
  // (there's probably a better way to this than storing the value on view load)
  self.initialFriendFieldWidth = self.friendField.intrinsicContentSize.width;
}

- (IBAction)friendFieldEditingChanged:(UITextField *)friendField {

  // Insert the friend name into the sentence
  NSString *sentencePart1 = @"I'm paying ";
  NSString *sentencePart2 = @"\n$ amount\nfor description";
  self.fullTextView.text = [@[sentencePart1, friendField.text, sentencePart2] componentsJoinedByString:@""];

  // Render the fullTextView, so that we can retrieve the friend name's last character position
  [self.fullTextView setNeedsLayout];
  [self.fullTextView layoutIfNeeded];

  // Retrieve the frame of the friend name's last character (in relation to the textView's coordinates)
  UITextPosition *last = [self.fullTextView positionFromPosition:self.fullTextView.beginningOfDocument offset:sentencePart1.length + friendField.text.length];
  UITextPosition *secondToLast = [self.fullTextView positionFromPosition:last offset:-1];
  UITextRange *range = [self.fullTextView textRangeFromPosition:secondToLast toPosition:last];
  CGRect rangeRect = [self.fullTextView firstRectForRange:range];

  // Here comes the trick:
  // The friendField's width will be reduced to the width of the caret and
  // placed at the last character's position within the fullTextView. 
  // This way the UITextView handles the display of the full text, 
  // incl. the parameter values. And the UITextFields will handle the input,
  // while only appearing as a caret.

  // Retrieve the caret width
  CGFloat width = [self.friendField caretRectForPosition:nil].size.width;
  // If no text is entered, unfold friendField to reveal the placeholder
  if (friendField.text.length == 0) {
    width = self.initialFriendFieldWidth;
  }

  // Using AutoLayout constraints (see Main.storyboard)
  // Note: A proper implementation needs to display the caret right where it is in 
  // relation to the input value. For now we only display it at the end of the value.
  self.friendFieldLeadingConstraint.constant = - rangeRect.origin.x - rangeRect.size.width;
  self.friendFieldTopConstraint.constant = - rangeRect.origin.y;
  self.friendFieldWidthConstraint.constant = width;
}

It will look like this: (highlighting the textfield dimensions)

Natural Language Form - 1. Default view - 2. With input

You can download a fully working example here: https://github.com/widescape/NaturalLanguageForm

Robert
  • 1,936
  • 27
  • 38
  • Allright that's really clever! However, from the docs I note that I can't allow multiple parameters right now. Could you also make a short video/document the instructions to create those constraints? The best approach would be to make a subclass/category of `UITextView` so it's the most flexible. I'll play around with it too and see if I can come up with something! Many thanks anyways :D – bdv Nov 21 '14 at 12:55
  • Never mind about the constraints, could easily check them in when I downloaded the project =) – bdv Nov 21 '14 at 13:02
  • You should be able to easily add further parameters. Just put more ```UITextFields``` in the Storyboard and duplicate the rules defining the top/leading constraints for them according to their input value ranges. – Robert Nov 21 '14 at 13:06
  • I just realized, that one To Do was still missing: Editing an existing value. But for that the attributed string just needs to define a link action to start editing the related UITextField. – Robert Nov 21 '14 at 13:09
  • Can you take it from here? – Robert Nov 21 '14 at 13:11
  • I think so :) I'll accept the answer when I've fixed the todo's! I'm curious about creating a category for this.. Do you also have a suggestion to only remove the newline when the `friendField.text` exits the `view.bounds`? – bdv Nov 21 '14 at 13:38
  • I guess, the behavior you want is not about removing/adding a newline, but that "$ amount" always stays together in a single line, right? – Robert Nov 21 '14 at 15:03
  • I'm not sure what you mean exactly, but I want something along this lines of: if strFriendLen + fixedTextLen > view.bounds: add last *word* on newline. – bdv Nov 21 '14 at 16:18
  • You could check if friendName spans multiple lines. If it does, you can remove the newline. If it doesn't keep the newline. Take a look at ```UITextView-firstRectForRange:``` to see how to check for multiple lines. But I'm not sure what view you refer with ```view.bounds```. – Robert Nov 21 '14 at 18:33
  • Thanks, I'm trying to create a more flexible solution though. I've made a subclass of `UITextField` and `UITextView` in order to make it a lot more flexible but there are obviously a couple of issues in subclassing this code, however I do feel like it's a good basis to start with. – bdv Nov 21 '14 at 20:57