Rightpoint / BonMot

Beautiful, easy attributed strings in Swift
MIT License
3.56k stars 197 forks source link

Can't use TextAlignmentConstraint within Interface Builder #355

Open mattcomi opened 5 years ago

mattcomi commented 5 years ago

Issue

I'm unable to use the TextAlignmentConstraint within Interface Builder.

Changing the custom constraint class to TextAlignmentConstraint has no effect and the attribute extra attributes do not appear in the attributes inspector. At runtime, this appears in the console:

Unknown class TextAlignmentConstraint in Interface Builder file.

This is a Swift project and I am trying to include the constraint within a UITableViewCell xib.

My Environment

I am using BonMot 5.3 via Carthage.

Things I've Tried

I can create the constraint in code no problem.

If I specify BONTextAlignmentConstraint for the custom class name in Interface Builder, it works but the extra attributes don't appear and it crashes at runtime on line 188 of TextAlignmentConstraint.swift:

let distanceFromTop1 = distanceFromTop(of: firstItem!, with: firstItemAttribute)

With:

Thread 1: Fatal error: Attempt to reason about unspecified constraint attribute

My suspicion is that Interface Builder can't find constraints within imported frameworks? If I create a custom constraint within my own project, it is able to find that. I tried this:

class MyTextAlignmentConstraint: TextAlignmentConstraint {}

But TextAlignmentConstraint is not open.

ZevEisenberg commented 5 years ago

Thanks for reporting this. On first reading, I believe this should be working. I’ll dig into it soon and get back to you. I don’t use Interface builder, so I wouldn’t be surprised if this had regressed. If I can fix it, I’ll add tests 😉

ZevEisenberg commented 5 years ago

Hey Matt,

Thanks again for your detailed bug report! I was able to reproduce your issue. I don't yet know what is causing it, but I have a little more info and two workaround for you.

Workaround 1

  1. You (apparently) have to use the Objective-C name of the class, which is BONTextAlignmentConstraint.
  2. You don't have to specify a module in Interface Builder.
  3. I havne't been able to get the @IBInspectable properties to work, but it does work if you pass the values manually in user-defined runtime attributes. You need to add entries for string properties called firstAlignment and secondAlignment, and you can pass any of these as values: top, cap height, x-height, first baseline, last baseline, bottom.

Workaround 2

Use CocoaPods. Unpopular opinion, perhaps, but I tested it and I was able to use the constraint as intended without any issues.

Unanswered Questions

I created a framework target inside my example project, and added a Swift file to it that looked like this:

import UIKit

@objc(PREFIXEDMyConstraint)
public class MyConstraint: NSLayoutConstraint {

    @IBInspectable var coolBeans: String?

    public override func awakeFromNib() {
        super.awakeFromNib()
        print("coolBeans:", String(describing: coolBeans))
    }
}

With that in place, I was able to refer to the class in Interface Builder as PREFIXEDMyConstraint, and set the inspectable properties without having to use user-defined runtime attributes. I don't know what the difference is with the Carthage-built framework, nor why it works in CocoaPods.

It looks like, at the very least, we should update the docs to make this more clear, but I'd love to find and fix the underlying issue.

ZevEisenberg commented 5 years ago

Here's a project that reproduces the issue. If you convert it over to using CocoaPods, you can use the constraint in IB without issue.

BonTest.zip

jdhealy commented 5 years ago

Yeah, it’s an Xcode limitation: anything pre-compiled (by Carthage or otherwise) doesn’t get the extra attributes in the attributes inspector.

You can remove the Carthage-compiled framework, use a workspace, and add and integrate (to that workspace) BonMot.xcodeproj. That should be all that’s necessary, but (with Xcode 10.1 and Zev’s BonTest.zip) Interface Builder can’t find the class for some reason — messing about, I kept seeing the error Unknown class __TtC6BonMot23TextAlignmentConstraint in Interface Builder file. There’s a couple workarounds, made a screencast showing one of them in effect:

https://archive.org/download/BONTextAlignmentConstraintScreencast/BONTextAlignmentConstraint%20Screencast.mp4

There’s another workaround involving editing BonMot source code, but honestly not really worth it…
That method — modifying `class TextAlignmentConstraint`: `@objc(BONTextAlignmentConstraint)` → `@objc // crossing fingers because we're no longer prefixing and clashes could result`. - With this a symbol `_OBJC_CLASS_$_BONTextAlignmentConstraint` disappears and is replaced by `_OBJC_CLASS_$__TtC6BonMot23TextAlignmentConstraint` — see `nm -gU BonMot.framework/BonMot | grep 'TextAlignmentConstraint'` _Honestly, not sure either this or the method from the video are worth the hassle. Just using the user-defined runtime attributes is ergonomic enough._