9

I'm building essentially a memorization app, where tapping a button on the keyboard takes the next word from a predetermined string, and adds it to the outputted text as shown below:

struct TypingGameView: View {
   @State var text: String = "Some text that wraps at the incorrect spot and is quite annoying."
   @State var outputText: String = ""
   @State private var enteredText: String = ""
   @State private var wordsTapped = 0

   var body: some View {
      ZStack {
         TextField("", text: $enteredText)
            .onChange(of: enteredText) { newValue in
               addToOutputText(newValue)
            }
         Rectangle()
            .foregroundColor(.white)
         VStack {
            Text(outputText)
               .frame(maxWidth: .infinity, alignment: .leading)
               .padding()
            Spacer()
         }
      }
   }

   func addToOutputText(_ val: String) {
      let words = text.components(separatedBy: " ")

      for (index, word) in words.enumerated() {
         if index == wordsTapped {
            outputText += "\(word) "
            wordsTapped += 1
            return
         }
      }      
   }
}

The problem is, the last word of the first line jumps to the next line only if there is one other word after it, but then jumps back to the first line once there are more words after that. See below:

Auto orphaning fix in action

To the best of my knowledge, this is an automatic feature of the Text view in SwiftUI to prevent there being any orphaned words. I want to disable this as it makes the words jump around in the view I've created. I've seen solutions with using a CATextLayer in UIKit (see UILabel and UITextView line breaks don't match and Get each line of text in a UILabel), but I need something that works with SwiftUI @State wrappers and would prefer a solution that uses all SwiftUI. End goal is to get same functionality as in above video, but with no automatic orphan fixing.

Edit: Just tried using Group with individual Text views inside for each word. Still does the same thing :/

YourManDan
  • 277
  • 1
  • 14
  • Have you found a way to overcome this? – demon9733 May 27 '22 at 19:22
  • 1
    @demon9733 The best I've been able to figure out so far is having all text displayed on screen at once, but set to `.foregroundColor(.clear)`, and then setting one word at a time to `.black` when the correct letter is typed. It feels quite hacky, takes up a lot of system resources and probably isn't great for accessibility, but it's the best I've got. – YourManDan May 28 '22 at 20:04

2 Answers2

3

To expand on Wamba's answer. You can use Zero Width spaces so that if the text is on a single line it won't have visible space after it, nor potentially wrap onto a third line.

/// This makes the text wrap so that it can have one word on the last line (a widow), by default iOS will leave an extra gap on the second to last line
/// and move the second to last word to the last line. With 2 line text this can make the text look like a column and make the UI look unbalanced.
public extension String {
    var fixWidow: String {
        self + "\u{200B}\u{200B}\u{200B}\u{200B}"
    }
}


// Use case:
Text("a short sentence that often wraps to two lines".fixWidow)

This is a hack, and I don't like it, but the app user can't see hacky code only a weird UI so this is preferable until Apple finally gives SwiftUI the same functionality as UIKit.

Jonathan.
  • 53,997
  • 54
  • 186
  • 290
  • Haha only getting back to this now a year later :P oddly enough, if you try both of yours and Wamba.'s solutions in the sample app I provided, they still have the same issue of automatically fixing the orphaned word. Even if I were to strip out everything except the `Text()` view, it still has the issue. – YourManDan Aug 26 '23 at 22:56
  • Aha! Turns out it does work after all. You just need to add enough of the zero width spaces depending on where the line breaks. Added like 15 to my string and it works beautifully. – YourManDan Aug 26 '23 at 23:14
2

Simply add three spaces to the end of the string:

let myString: String = "Modo ligth (colores predefinidos claros)."

Text(myString)

Without spaces

let myString: String = "Modo ligth (colores predefinidos claros)."
let myNewString: String = myString + "   " // <- Three additional spaces

Text(myNewString)

With additional spaces

Wamba
  • 59
  • 6
  • 1
    It works!, It's a hack but what can you expect with SwiftUI... – Jonathan. Nov 30 '22 at 13:59
  • Haha only getting back to this now a year later :P oddly enough, if you try both of yours and Jonathan.'s solutions in the sample app I provided, they still have the same issue of automatically fixing the orphaned word. I got your line of text to work in one scenario but I think it might've been because of the parentheses in the last word. – YourManDan Aug 26 '23 at 22:54
  • Wait! Never mind! It seems to work, but you need to add a lot more spaces depending on where the line breaks. I tried it again but with about 15 spaces and that works beautifully. If you could edit your answer with that additional information, then I'll mark your answer as correct. – YourManDan Aug 26 '23 at 23:07