How do you make a vertical text UILabel and UITextView for iOS in Swift?
Asked Answered
R

2

48

If you came to this question based on the title but are not interested in Mongolian, you might be looking for this Q&A instead:


I've been learning Swift in order to develop iOS apps for traditional Mongolian. The problem is that traditional Mongolian is written vertically from top to bottom and from left to right. My question is how do I display text vertically and still have line wrapping work?

If you stay with me for a minute, I'll try to explain the problem more clearly. Below is an image of the kind of text view I would like to achieve. In case the foreign script throws you off, I have included English text that follows the same pattern. In fact, if the app has any English text to display, this is how it should look.

enter image description here

For a simple one-line UILabel, a 90 degree clockwise rotation would work. However, for a multi-line UITextView I need to deal with line wrapping. If I just do a plain 90 degree rotation, the first thing written will end up being on the last line.

So far I have made a plan that I think can overcome this problem:

  1. Make a custom font in which all of the letters are mirrored vertically.
  2. Rotate the text view 90 degrees clockwise.
  3. Mirror the text view horizontally.

enter image description here

That should take care of the text wrap.

I can do the mirrored font. However, I don't know how to do the Swift coding for the rotation and mirroring of the UITextView. I've found the following links that seem to give hints to parts of the solution, but they are all in Objective C and not in Swift.

There are traditional Mongolian apps in the app store (like this and this) but I haven't found anyone yet who is sharing their source code, so I don't know what they are doing behind the scenes to display the text. I plan to make my code open source so that it is not so hard for others in the future to develop apps for the several million people who read traditional Mongolian. Any assistance you can give to this endeavor would be much appreciated, not just by me but also by the Mongolian people. Even if you don't know yourself, upvoting this question to make it more visible would help.

Update

@sangonz's answer is still a great answer, but I temporarily unmarked it as the accepted answer because I just couldn't get everything to work. Specifically:

  • Enabling scrolling (either by embeding the custom view in a scrollview or by subclassing UIScrollView). In the github project, @sangonz said this should be easy, but it wasn't for me.
  • Getting a relayout (rather than stretching) of the word lines on an orientation change. I think this shouldn't be too hard to solve with a little more research.
  • Why don't the text lines go all the way to the edge of the view? There is a big gap at the bottom.
  • How to unlink the NSTextStorage of the custom vertical view from the other UITextView. (see this question)

Up to this point I have been using the original method I proposed above, but what I really want is to get something like what @sangonz proposed working.

I am also now considering alternate methods like

  • Using Core Text, Disadvantage: it feels like reinventing the wheel
  • Using WebKit, Disadvantage: Apple no longer uses WebKit for their UITextView
Refuge answered 16/2, 2015 at 15:24 Comment(5)
Probably you will find the proper way to do this in TextKit.Hellhound
This information is embedded in the text font and script.Unreality
Similar to OpenType fonts, AAT fonts can support the ligatures and complex text rendering that is requited to properly display traditional Mongolian words. However, that support does not extend to making the text display vertically. The Mongolian glyphs embeded in the font are written sideways and are meant to be rotated later by the software that uses the font. (But if I am missing a major function of these fonts then please tell me more. That would certainly be much easier than what I am proposing in my question.)Refuge
are you open to rendering in a web view? I believe there is css you can apply to render text like this.Neuromuscular
@TomSwift, I haven't tried a web view partly because I have limited experience with css/html/etc and partly because Apple separated its own UITextView from the underlying WebKit. However, I am open to trying anything that works. It could be that using a web view would make this approach more platform independent.Refuge
M
21

Edit: This is how I finally did it.

Here's a very basic implementation in my GitHub: Vertical-Text-iOS.

Nothing fancy, but it works. Finally I had to mix TextKit and image processing. Take a look at the code. It involves:

  1. Subclassing NSTextContainer to get the right text dimensions.
  2. Creating a custom UIView to render the text applying affine transformations to each line and rendering using NSLayoutManager to keep all TextKit features.

TextKit way

The proper way to keep all native text benefits (e.g. highlighting, selection...) is to use standard TextKit APIs. The method you are proposing would break all that or would possibly result in strange behaviour.

However, looks like TextKit in iOS does not support vertical orientation out-of-the-box yet, but it is prepared for that. As a side note, in OS X it is somewhat supported and you could call textView.setLayoutOrientation(.Vertical), but it still has some limitations.

The NSTextLayoutOrientationProvider protocol defines an interface providing the default orientation for text laid out in a conforming object, in absence of an explicit NSVerticalGlyphFormAttributeName attribute. The only UIKit class that implements this interface is NSTextContainer, whose default implementation returns NSTextLayoutOrientationHorizontal. An NSTextContainer subclass that handles vertical text could set this property to NSTextLayoutOrientationVertical to support the custom layout orientation logic.

Source: UIKit > NSTextLayoutOrientationProvider Protocol Reference for iOS

In conclusion, you should start subclassing NSTextContainer, and you will have to deal with NSLayoutManager and NSTextContainer a lot.


Custom image processing way

If, on the other hand you decide to follow your custom text rendering I suggest the following approach.

  1. Render the normal text to a hidden layer with a normal font. Give it the correct size to its bounding box.
  2. Get the text properties, mainly text height and line spacing.
  3. Process the image drawing each line in reverse order from bottom to top as you can see in the image below. You should get a new CGImage as a result.
  4. Rotate the image creating a UIImage and setting the correct UIImageOrientation value.
  5. Insert that image into a UIScrollView that only allows horizontal scrolling.

Beware this method renders the whole text, so don't use it for very long texts. If you need to do that, you will need to consider a tiling approach. Watch WWDC 2013 > 217 - Exploring Scroll Views on iOS 7.

this image

Good luck!

Update: (image from github project)

enter image description here

Malathion answered 24/2, 2015 at 11:43 Comment(7)
Also make your solution/work-in-progress public in GitHub. I'm sure many people will find it useful and improve over it.Redintegration
Definitely +1 to this answer for giving two alternative strategies for solving this problem, as well as for the carefully crafted answer. Your first idea sounds like the best since I will need to support long texts. However, my reason for posting this question is that I am new to iOS and lack the Swift programming skills to implement any strategy, mine or yours. So I am looking for the actual code to get it done. I will certainly keep experimenting and plugging away at this until I get something that works.Refuge
Thanks for the suggestions. I've edited my comment and linked some sample code. Hope it helps!Malathion
Wow, thanks for putting all the work into that! I will study it carefully. One quick question: You said, "The code does not follow good coding practices. Don't use it in production code as-is." Briefly, what general principles would you follow to make it have "good coding practices."Refuge
I'm afraid that's not something that can be answered briefly. It mostly concerns with a thorough error checking, having each class in a different file... those kinds of things.Malathion
I was trying to get this to work before but couldn't and now I am making another try at it. I took the liberty of editing your answer in order to show the screen shot you had in your github project. My question now is why is there a big space at the bottom of the VerticalTextView? Also, when I rotate the device the line is just squeezed to fit the new hight. I can't figure out how to get the line to reset the number of words that it should contain.Refuge
I opened a new question about this.Refuge
S
4

If you're going to rotate the text I would suggest using a right-to-left layout so that you can skip the mirroring step (and just rotate the other way).

You should be able to just set the label/textview's transform property:

view.transform = CGAffineTransformTranslate(CGAffineTransformMakeRotation(CGFloat(-M_PI_2)), view.bounds.width, view.bounds.height)

You need to translate after you rotate because the view rotates around its origin (in the upper left).

The good news is that gestures and taps are transformed at the same time the pixels are, so controls continue to work the way you expect them to.

Saphra answered 24/2, 2015 at 12:53 Comment(2)
A problem that I've had with right-to-left layouts (albeit with Android) is that they are designed for Arabic, Hebrew, and other RTL languages. These languages are actually bidirectional because numbers and English (and Mongolian) words which are embedded in the text are still written LTR. And even if it were truly RTL, the words would be spelled backwards and for Mongolian the glyphs would not match to the next letter (unless I used a horizontally mirrored font). The code you posted was quite helpful for putting me on the right track with rotation and translation. I'm experimenting more now.Refuge
I wrote this answer just now to help myself learn how to do this: https://mcmap.net/q/23399/-how-can-you-rotate-text-for-uibutton-and-uilabel-in-swiftRefuge

© 2022 - 2024 — McMap. All rights reserved.