Search code examples
swiftswiftui

Is there a way to disable SwiftUI's Text automatic orphaning fix?


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 :/


Solution

  • 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.