0

Xcode 10, Swift 5, iOS 12

I've got two UILabels in a horizontal StackView:

  • StackView: Alignment Fill & Distribution Fill
  • Label 1: No special constraints
  • Label 2: Proportional Width (half the size of Label 1)

Label 2 contains a word that's too long for the label size on an iPhone SE (smallest supported device), so I'm using an abbreviated version. On a bigger device, e.g. an iPad, I want to display the full word (should only be set once), so I tried this:

var label2set:Bool = false
print("label2: \(label2.frame.width)")

if !label2set && label2.frame.width > 100 && (UIApplication.shared.statusBarOrientation == .portrait || UIApplication.shared.statusBarOrientation == .portraitUpsideDown) {
    label2set = true
    label2.text = "VeryLongLabelText"
}

No matter if I use this code in viewDidLoad or viewWillAppear, the first time it's called the supposed label width is only about 70 (even on an iPad), even though it's clearly bigger in the simulator.

If I put the code in viewWillAppear and remove the check for label2set, then switch to the next view through my NavigationController and go back to my original one again, the code is called properly and the label displays the full text (width: about 200 on an iPad).

When do labels actually get set to their proper width, so when and how can I check the size?

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Neph
  • 1,823
  • 2
  • 31
  • 69
  • why don't you use minimum font scale property. – Abu Ul Hassan Apr 30 '19 at 12:43
  • @AbuUlHassan I want both labels to be the same height, changing the (font) size of just label2 would look weird and label1 is wide enough for its text, so there's no point changing that one too. – Neph Apr 30 '19 at 12:47
  • after settings text to label1 just use the font attributes of label one on label2 ;) simple – Abu Ul Hassan Apr 30 '19 at 12:49
  • @AbuUlHassan I think you misunderstood my question. The text size isn't the problem, both texts fit inside their labels, I just want to display a longer text for label2 if the label's big enough (which it will be with higher resolutions/bigger devices). Imo changing the text size just to make something fit is a bad solution because it's simply inconsistent. – Neph Apr 30 '19 at 12:54

2 Answers2

0

If you really need such functionality why don't you just use idiom check and change label text itself?

if UIDevice.current.userInterfaceIdiom == .phone {
    // ...
} else {
    // ...
}

You can put your code inside viewDidLayoutSubviews method:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    // do something...
}

EDIT:

viewDidLayoutSubviews will be called every time you have made changes to view controller's view or subviews... changes that concern view controller's view. I think in your case it's OK when it's called multiple times as when user rotates device you want to check if layout has changed in such a way that you now need to show shorter/longer text. Also when other view elements have autolayout constraints defined and you change something with them that may result your label to shrink/grow etc.

EDIT 2:

When using auto layout you basically can't know what size your label has ... when you check for width in tableViewCellForRowAt() you get with you used in story board or Nib file.

I suggest you just use two things for determining which version of text to use ... overall window width/height and current orientation (and width class if you support split view). In this case you know right before you set cell label which text you should set. You just have to decide if at current orientation and at overall width value, should you switch to shorter text.

jannolii
  • 86
  • 2
  • 9
  • It's not about distinguishing between phones and tablets, with phones getting bigger and bigger it's possible that the resolution of one is high enough to fully display the text (with an Xs Max the width is 98pts). I tested your solution and it works, thanks. I did notice though that `viewDidLayoutSubviews` is called twice: 1. With the wrong/old length and 2. with the right one. And also every time the orientation changes but it doesn't print the right value until after the next change (so landscape: 200 - portrait: 300). Is there any function that already skips this first (unnecessary) check? – Neph Apr 30 '19 at 11:38
  • I would recommend that you drop such customization and redesign you interface to use more autolayout and same texts for different devices. Because right now it’s about “fighting the sustem” (which of course doesn’t mean that it can’t be done). Regarding your question... as you don’t provide much code, I can’t actually say what triggers the function. But like I said... you are fighting the system :) – jannolii Apr 30 '19 at 16:06
  • Sorry for the late reply. I would use the same text but my app's going to support everything from the iPhone SE (4") to the iPad Pro (12") and while I'm building for the SE and then let autolayout do its work for use on iPads, there's simply some stuff (like the text in my question) that has to change depending on the available space. Plus, it would look weird if there's enough space and I'm using the abbr. text So no, not fighting the system, just using the functions it provides. ;) Ad question: The function is called twice at the very start: Once before autolayout does its thing, once after. – Neph May 07 '19 at 09:59
  • You can't control how many times viewDidLayoutSubviews is called... it's called when it needs to be called (https://stackoverflow.com/a/32688124/2099540). Let it be called twice :) You need this function anyway... if user interactivity does something else with the layout. If you want better answer you'll have to share more code ... like whole view controller and storyboard with your layout constraints, components etc – jannolii May 07 '19 at 12:48
  • Thanks for the link. According to it, 'viewDidLayoutSubviews' is called once the views are set up but apparently this doesn't work with orientation. When it's called after changing the orientation it still reports the old one, which means that after changing the orientation twice and switching to the other view with the NavigationController, the wrong label text is used. How do I prevent `viewDidLayoutSubviews` from getting called on orientation change? Unless I knew for sure that the func's always triggers twice, I can't even check `if timestriggered < 2`. – Neph May 08 '19 at 09:22
  • Sorry, I can't share the whole storyboard or code. The basic layout of the view you can see [here](https://i.stack.imgur.com/PJskA.png) (old version) - I added another view on top (outside the table) that contains the two labels (like a header) but they look the same as the two inside the table. I've got a `viewDidLoad` (set delegate/datasource of table), a `viewWillAppear` (change tabledata related stuff, incl. reloading it if necessary) and a `viewDidLayoutSubviews` (which I'm trying to get this stuff to work with). There are two buttons but they or the table don't interfere with the labels. – Neph May 08 '19 at 09:30
  • I've now tested it with `viewDidAppear` (`if !alreadySetLabel && label.frame.width > 100 && (UIApplication.shared.statusBarOrientation == .portrait || UIApplication.shared.statusBarOrientation == .portraitUpsideDown)`): It's only called once and it gets the right width but unfortunately you can see the label change. :/ Is there anything that's called inbetween `viewDidLayoutSubviews` and `viewDidAppear` that gets the right width on the first try (don't care if it's called multiple times after that)? – Neph May 08 '19 at 11:09
  • Ok. I looked at the picture you linked to. You previously failed to mention that your stack view is inside of table view cell. I don't know where exactly you define your cell (Nib, Class, Storyboard etc) but when using auto layout you basically can't know what size your label has. I suggest you just use two things for determining which version of text to use ... overall window width and orientation (and width class if you support split view). In this case you know right before you set cell label which text you should set. – jannolii May 09 '19 at 11:51
  • @Neph, did you get it working? What did you end up using? – jannolii May 27 '19 at 07:14
  • Sorry, I've had to deal with other parts of my project and haven't had time to try your suggestion about window width yet. My question isn't about the StackView in the cell though but about one above (and outside) the table (think of it as a header for the table) - see my second to last comment from May 8th for a description. – Neph May 27 '19 at 12:00
  • 1
    Upvoted and accepted for `viewDidLayoutSubviews`. In combination with `if self.view.frame.width >= 375` the label text is set properly now, no matter if I start in landscape mode or if I switch orientation while on a different screen that's connected to the original one through a NavigationController. No need to check for the orientation because in landscape mode the width returns a higher value anyway. Width of devices (in portrait): SE/5S: 320pts, 6/X: 375pts, 7+/8+/Xs Max: 414pts – Neph Jun 26 '19 at 11:51
0

Use stringsDict file for this. Create a stringsdict type file. follow this format:

<key>Login</key>
<dict>
    <key>NSStringVariableWidthRuleType</key>
    <dict>
        <key>100</key>
        <string>Login.</string>
        <key>200</key>
        <string>You must login before continuing.</string>
        <key>300</key>
        <string>Please enter your username and password to continue.</string>
    </dict>
</dict>

and later somewhere in code:

let localized = NSLocalizedString("Login", comment: "Prompt for user to log in.") as NSString

label.text = localized.variantFittingPresentationWidth(300)

NOTE: You can pass any integer you want into variantFittingPresentationWidth()

iOS will automatically resolve it to find the best match in your strings dictionary, counting downwards where necessary. For example, if you tried loading a string with width 500, the 300 string would be returned, but if you tried 299 then the 200 string would be returned.

Also extracting all strings from swift files into separated files like stringsdict is always best practice.

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • It is the most reliable way since apple continuously make change to internal Layout framework and etc. I thing 1 extra file is not overkill compared to 5 `if` conditions and some extra `flag`s you defined in your approach. – Mojtaba Hosseini Apr 30 '19 at 11:49
  • My initital comment: I still have to test your solution but isn't this a bit overkill? I only need a single check after starting the app and afterwards it's going to stay the same until you restart the app. Am I right in my assumption that your code also changes the text if the orientation of the device changes? – Neph Apr 30 '19 at 11:50
  • It will change on device change, orientation change, window size change (in iPad split view) and any future way. When text needs more space than the label's frame, it will change to smaller version. – Mojtaba Hosseini Apr 30 '19 at 11:52
  • Could you clarify how I know what integer to pass to `variantFittingPresentationWidth`? If I have to just pass the current width of the label, I'm back to square one because I won't get the right width with `viewDidLoad` or `viewWillAppear`. – Neph Apr 30 '19 at 11:53
  • Keys represents minimum width needed for the text (based on your design). – Mojtaba Hosseini Apr 30 '19 at 11:58
  • `variantFittingPresentationWidth(<#MaxWidth#>)` gets maximum width you need from the file. – Mojtaba Hosseini Apr 30 '19 at 11:59
  • If the width is < 100, I want it to use "Text", otherwise "VeryLongLabelText". If the app is run on an iPad, the width of the label is probably something between 200 and 300 but there's no maximum. So basically I use "Test" with the key "0" and "VeryLongLabelText" with "100" and pass "100" to `variantFittingPresentationWidth`? How do I prevent it from switching if the orientation changes,...? – Neph Apr 30 '19 at 12:09
  • I just tested it with what I mentioned in my last comment and also added this to `viewDidLoad`: `let localized = NSLocalizedString("Test", comment: "") as NSString` and `label2.text = localized.variantFittingPresentationWidth(100)` - but no matter what device I use in the simulator, it always chooses the long text, which is then, of course, truncated. The same thing also happens if I pass "99" instead of "100". – Neph Apr 30 '19 at 12:36
  • You should assign sizes very wisely. – Mojtaba Hosseini Apr 30 '19 at 14:16
  • `You should assign sizes very wisely` - Sorry, I don't understand. Does your suggestion work for what I want (short text if <100, otherwise long text)? What I've tried (see previous 2 comments) didn't and I'm not sure if I used the wrong code or if `variantFittingPresentationWidth` simply isn't intended for what I want it to do. – Neph Apr 30 '19 at 14:28
  • I really wanted this to work, but as far as I can tell UILabel uses the size of the window it is in to determine what variant width to search for (it uses the size of the window divided by the em width of the font). That would be useful to select different strings based on rotation, current iPad splitting, and display size...but NOT the available width of the UILabel which is what I really really want (and what I think this question is looking for) – Stripes Sep 11 '19 at 21:34
  • The available width can be full width or any constant width you set on the label with autolayout constraint or stackview or etc. @Stripes – Mojtaba Hosseini Sep 11 '19 at 21:37
  • Sure, but what I want is to set a UILabel to say 75% of the width of the enclosing view, or something else moderately complex (and I know how to do ALL those things), and set the text to some NSString (that has multiple width variants), and let the UILabel choose the largest variant that fits...and not need my app to come back and do more work if the device rotates and now that string doesn't fit, OR a longer one fits. I don't want to need to do more work if that label is inside a CollectionView cell and the cell shrinks when some more values get added to the collection...or whatever – Stripes Sep 11 '19 at 21:40
  • So call `variantFittingPresentationWidth` on `didLayoutSubViews` then. (or in `layoutSubviews` after calling the `super.layoutSubviews`) – Mojtaba Hosseini Sep 11 '19 at 21:45
  • I'm not the caller: UILabel is. If I need to make a UIView manage "things" on layout I would just make a UILabel subclass that takes an array of strings (or attributed strings) and select the one that fits. Messing around with NSStringVariableWidthRuleType objects really only makes sense if the framework does something itself with them. – Stripes Sep 11 '19 at 21:52
  • I think the label you are talking about is inside a `UICollectionViewCell`. I meant to override `layoutSubviews` of that. (Also can be done on any parent view of the label) – Mojtaba Hosseini Sep 11 '19 at 21:54