qt自定义界面生成器
This article is originally published at https://swiftsenpai.com on August 22, 2020
本文最初于 2020年8月22日 发布在 https://swiftsenpai.com 上
Since the publishing of my article “UICollectionView List with Custom Cell and Custom Configuration”, a lot of my readers have been asking me whether it is possible to design the custom UICollectionViewListCell
in the interface builder.
自从发表文章“ 具有自定义单元格和自定义配置的UICollectionView列表 ” 以来 ,许多读者一直在问我是否可以在界面构建器中设计自定义UICollectionViewListCell
。
Well, the answer is definitely yes!
好吧,答案肯定是肯定的 !
Since the UICollectionViewListCell
's content view is just a UIView
subclass that conforms to the UIContentView
protocol, theoretically as long as we are able to link up the content view with the interface builder then everything should work accordingly.
由于UICollectionViewListCell
的内容视图只是符合UIContentView
协议的UIView
子类,因此UIContentView
理论上讲,只要我们能够将内容视图与接口生成器链接起来,则所有内容都应相应地工作。
After spending some time experimenting in Xcode, I have successfully created a UICollectionViewListCell
content view using interface builder. On top of that, I also discovered that you can even manipulate the cell height by adjusting the auto layout constraints within the cell's content view.
花了一些时间在Xcode中进行实验之后,我已经使用界面构建器成功创建了UICollectionViewListCell
内容视图。 最重要的是,我还发现您甚至可以通过调整单元格内容视图中的自动布局约束来操纵单元格高度。
How can this be done? Let’s find out.
如何才能做到这一点? 让我们找出答案。
示例应用 (The Sample App)
For simplicity’s sake, our sample app will show a simple custom cell that contains only 1 label. Here’s the screenshot of the sample app:
为简单起见,我们的示例应用程序将显示一个仅包含1个标签的简单自定义单元格。 这是示例应用程序的屏幕截图:
Note that I also added a red border to the label so that it is easier for us to observe its height later in this article.
请注意,我还在标签上添加了红色边框,以便我们稍后在本文中更容易观察它的高度。
With all that being said, let’s begin.
综上所述,让我们开始吧。
Note:
注意:
I won’t get into too much detail on the concept behind content view, content configuration, and custom cell. If you are not familiar with that, I highly recommend you to go through my previous article “ UICollectionView List with Custom Cell and Custom Configuration “ before proceeding.
我不会在内容视图,内容配置和自定义单元格后面详细介绍概念。 如果您对此不熟悉,强烈建议您先阅读上一篇文章“ 具有自定义单元格和自定义配置的UICollectionView列表 ”,然后再继续。
实施自定义内容视图和内容配置 (Implementing Custom Content View and Content Configuration)
First thing first, let’s implement the custom content view and content configuration. Let’s name both of them SFSymbolNameContentView
and SFSymbolNameContentConfiguration
.
首先,让我们实现自定义内容视图和内容配置。 让我们将它们都命名为SFSymbolNameContentView
和SFSymbolNameContentConfiguration
。
Here’s how we will implement the SFSymbolNameContentView
class:
这是我们将如何实现SFSymbolNameContentView
类的方法:
class SFSymbolNameContentView: UIView, UIContentView {
// 1
// IBOutlet to connect to interface builder
@IBOutlet var containerView: UIView!
@IBOutlet var nameLabel: UILabel!
private var currentConfiguration: SFSymbolNameContentConfiguration!
var configuration: UIContentConfiguration {
get {
currentConfiguration
}
set {
// Make sure the given configuration is of type SFSymbolNameContentConfiguration
guard let newConfiguration = newValue as? SFSymbolNameContentConfiguration else {
return
}
// Set name to label
apply(configuration: newConfiguration)
}
}
init(configuration: SFSymbolNameContentConfiguration) {
super.init(frame: .zero)
// 2
// Load SFSymbolNameContentView.xib
loadNib()
// Set name to label
apply(configuration: configuration)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension SFSymbolNameContentView {
private func loadNib() {
// 3
// Load SFSymbolNameContentView.xib by making self as owner of SFSymbolNameContentView.xib
Bundle.main.loadNibNamed("\(SFSymbolNameContentView.self)", owner: self, options: nil)
// 4
// Add SFSymbolNameContentView as subview and make it cover the entire content view
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0.0),
containerView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0.0),
containerView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0.0),
containerView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0.0),
])
// 5
// Add border & rounded corner to name label
nameLabel.layer.borderWidth = 1.5
nameLabel.layer.borderColor = UIColor.systemPink.cgColor
nameLabel.layer.cornerRadius = 5.0
}
private func apply(configuration: SFSymbolNameContentConfiguration) {
// Only apply configuration if new configuration and current configuration are not the same
guard currentConfiguration != configuration else {
return
}
// Replace current configuration with new configuration
currentConfiguration = configuration
// Set name to label
nameLabel.text = configuration.name
}
}
From the above code, there are several important parts that we must take note:
从上面的代码中,我们必须注意几个重要部分:
Define
IBOutlet
for the label and container view. Note that the container view will be our interface builder canvas, we will be working on that in just a moment.为标签和容器视图定义
IBOutlet
。 请注意,容器视图将是我们的界面构建器画布,我们将在稍后对此进行处理。Call the
loadNib()
function during initialization to configureSFSymbolNameContentView
accordingly.在初始化期间调用
loadNib()
函数以相应地配置SFSymbolNameContentView
。Within the
loadNib()
function, load a nib file namedSFSymbolNameContentView.xib
and makeself
as the owner of the nib file.在
loadNib()
函数中,加载一个名为SFSymbolNameContentView.xib
的笔尖文件,并使其self
成为该笔尖文件的所有者。- Make the container view take up the entire content view using auto layout constraints. 使用自动布局约束使容器视图占据整个内容视图。
- Add a red color border and rounded corners to the label. 在标签上添加红色边框和圆角。
Next up, we will work on the implementation of SFSymbolNameContentConfiguration
class. The implementation is pretty straightforward, we just need to make sure the makeContentView()
is returning an instance of SFSymbolNameContentView
.
接下来,我们将研究SFSymbolNameContentConfiguration
类的实现。 该实现非常简单,我们只需要确保makeContentView()
返回SFSymbolNameContentView
的实例SFSymbolNameContentView
。
struct SFSymbolNameContentConfiguration: UIContentConfiguration, Hashable {
var name: String?
func makeContentView() -> UIView & UIContentView {
// Initialize an instance of SFSymbolNameContentView
return SFSymbolNameContentView(configuration: self)
}
func updated(for state: UIConfigurationState) -> Self {
return self
}
}
在Interface Builder中创建自定义UIView
(Creating Custom UIView
in Interface Builder)
With both content view and content configuration classes in place, let’s switch our focus to the interface builder. Here’s what we are going to do:
有了内容视图和内容配置类,让我们将重点转移到界面构建器上。 这是我们要做的:
- Create a new user interface view. 创建一个新的用户界面视图。
- Set the file’s owner’s class. 设置文件所有者的类。
- Resize the custom view. 调整自定义视图的大小。
- Add a label and configure auto layout. 添加标签并配置自动布局。
Connecting all
IBOutlet
s.连接所有
IBOutlet
。
1.创建一个新的用户界面视图 (1. Create a New User Interface View)
In Xcode, press ⌘N (cmd + N) to add a new file to your project. In the add new file dialog, select “View” under the user interface section, click “Next” and name the nib file SFSymbolNameContentView
.
在Xcode中,按⌘N(cmd + N)将新文件添加到项目中。 在“添加新文件”对话框中,在用户界面部分下选择“查看”,单击“下一步”,然后将笔尖文件命名为SFSymbolNameContentView
。
2.设置文件所有者的类 (2. Set the File’s Owner’s Class)
Next, we must let the interface builder know that the nib file we just created is owned by the SFSymbolNameContentView
class.
接下来,我们必须让接口构建器知道我们刚刚创建的nib文件由SFSymbolNameContentView
类拥有。
Inside the interface builder left panel, click on “File’s Owner” and then within the attribute inspector, set the file’s owner’s class to SFSymbolNameContentView
.
在界面构建器左面板内,单击“文件的所有者”,然后在属性检查器中,将文件的所有者的类设置为SFSymbolNameContentView
。
3.调整自定义视图的大小 (3. Resize the Custom View)
By default, the interface builder will create a custom view following the iPhone form factor. However, this is not what we want, we need a view much smaller than that.
默认情况下,界面构建器将按照iPhone尺寸创建一个自定义视图。 但是,这不是我们想要的,我们需要的视图要小得多。
Go ahead and open the view’s attribute inspector and change the size simulated metrics to “ freeform “.
继续并打开视图的属性检查器,并将大小模拟指标更改为“ 自由格式 ”。
After that, open the size inspector and set the view height to 150pt.
之后,打开尺寸检查器并将视图高度设置为150pt 。
4.添加标签并配置自动布局 (4. Add a Label and Configure Auto Layout)
Now drag a UILabel
into the view and set its top, bottom, leading, and trailing auto layout constraint according to the image below.
现在,将UILabel
拖动到视图中,并根据下图设置其顶部,底部,前导和尾随自动布局约束。
This will make the UILabel
take up the entire view.
这将使UILabel
占据整个视图。
5.连接所有IBOutlets (5. Connecting All IBOutlets)
Lastly, connect the containerView
and nameLabel
IBOutlet
to the interface builder as shown in the image below:
最后,将containerView
和nameLabel
IBOutlet
连接到界面生成器,如下图所示:
With that, we have successfully connected the SFSymbolNameContentView
class to the interface builder. Let's go ahead and run your sample app to see everything in action.
这样,我们就成功地将SFSymbolNameContentView
类连接到了界面构建器。 让我们继续运行您的示例应用程序,以查看运行中的所有内容。
But wait! The cell height is different from what we expected! 😨
可是等等! 单元高度与我们预期的不同! 😨
In the next section, let’s find out what causes this problem and how we can fix it.
在下一节中,让我们找出导致此问题的原因以及如何解决它。
处理单元格高度 (Dealing with Cell Height)
The reason behind the unexpected cell height is because UICollectionViewListCell
is self-sizing by default.
意外的单元格高度背后的原因是因为UICollectionViewListCell
默认情况下是自动调整大小的。
Since we do not specify the height constraint of the nameLabel
and we only configure the nameLabel
to take up the entire containerView
, the collection view will automatically resize the cells based on the nameLabel
's content.
由于我们未指定nameLabel
的高度约束,而仅将nameLabel
配置为占用整个containerView
,因此收集视图将根据nameLabel
的内容自动调整单元格的大小。
The solution to fix the problem is pretty straightforward. We just need to add a height constraint to the nameLabel
so that the auto layout engine will take into account the nameLabel
's height when calculating the cell's height.
解决该问题的解决方案非常简单。 我们只需nameLabel
添加一个高度约束,以便自动布局引擎在计算单元格的高度时将考虑nameLabel
的高度。
nameLabel
nameLabel
添加高度约束
If you run your sample app now, you should see that the cell has been resized correctly. Unfortunately, by adding a height constraint to the nameLabel
has caused unsatisfiable constraints within the cell.
如果现在运行示例应用程序,则应该看到该单元格已正确调整大小。 不幸的是,通过将高度限制添加到nameLabel
导致了单元内nameLabel
满足的限制 。
Based on the Xcode console output, it seems like the nameLabel
's height constraint is conflicting with the content view's height constraint.
根据Xcode控制台的输出,似乎nameLabel
的高度约束与内容视图的高度约束冲突。
“But we never set any height constraint on the content view!” you might say.
“但是我们从未在内容视图上设置任何高度限制!” 你可能会说。
I am not 100% sure on why there is a height constraint being set on the content view, I suspect UIKit
is giving the content view a default 44pt height constraints before calculating its actual height.
我不确定为什么在内容视图上设置了高度限制,我不确定100%,我怀疑UIKit
在计算实际高度之前会给内容视图默认的44pt高度限制。
Since we set our nameLabel
to have 126pt height, but the content view can only have 44pt height, it is not possible to simultaneously satisfy both of these constraints, thus causing the unsatisfiable constraints issue.
由于我们将nameLabel
设置为具有126pt的高度,但是内容视图只能具有44pt的高度,因此无法同时满足这两个约束,从而导致无法满足的约束问题。
To resolve this issue, we can reduce the nameLabel
's height constraint priority to 999 so that the auto layout engine can temporarily ignore this constraint.
要解决此问题,我们可以将nameLabel
的高度约束优先级降低为999,以便自动布局引擎可以暂时忽略此约束。
nameLabel
height constraint priority to 999
nameLabel
高度限制优先级设置为999
This article discusses in great details the solution we use to resolve the unsatisfiable constraints issue. Feel free to check it out if you want to know more.
本文非常详细地讨论了我们用来解决无法满足的约束问题的解决方案。 如果您想了解更多信息,请随时查看。
With that, we have successfully designed a custom UICollectionViewListCell
using an interface builder. 🥳
这样,我们已经使用界面构建器成功设计了一个自定义UICollectionViewListCell
。 🥳
You can get the full sample project here.
您可以在此处获得完整的示例项目。
一个常见的例外 (One Common Exception)
When you design a custom UICollectionViewListCell
in an interface builder, one most common exception you might encounter is the ' NSInternalInconsistencyException '.
在界面生成器中设计自定义UICollectionViewListCell
时,您可能会遇到的最常见的异常是“ NSInternalInconsistencyException ”。
The exception message looks something like this in the Xcode console:
在Xcode控制台中,异常消息如下所示:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Rounding frame ({{20, -8.9884656743115785e+307}, {374, 1.7976931348623157e+308}}) from preferred layout attributes resulted in a frame with one or more invalid members ({{20, -8.9884656743115785e+307}, {374, inf}}). Layout attributes: <UICollectionViewLayoutAttributes: 0x7fd0d2043130> index path: (<NSIndexPath: 0x947fdb8ab9f8e625> {length = 2, path = 0 - 0}); frame = (20 -8.98847e+307; 374 1.79769e+308); zIndex = 10; View: <Swift_Senpai_UICollectionView_List.SFSymbolNameListCell: 0x7fd0cfc21470; baseClass = UICollectionViewListCell; frame = (20 17.5; 374 44); clipsToBounds = YES; layer = <CALayer: 0x600002768000>>'
This kind of exception is due to the auto layout engine not having sufficient information to determine the cell’s height. It is usually caused by missing vertical spacing constraints and missing height constraints within the content view. Therefore, when it happens, make sure to recheck the auto layout configurations of your content view’s content.
这种例外是由于自动布局引擎没有足够的信息来确定单元的高度。 这通常是由于内容视图中缺少垂直间距约束和高度约束而导致的。 因此,当发生这种情况时,请确保重新检查内容视图内容的自动布局配置。
结语 (Wrapping Up)
The concept presented in the “ Creating Custom UIView in Interface Builder” section is basically connecting a UIView
subclass to the interface builder. Therefore, this technique is not limited to just for creating a custom UICollectionViewListCell
.
“ 在Interface Builder中创建自定义UIView ”一节中介绍的概念基本上是将UIView
子类连接到Interface Builder。 因此,此技术不仅限于创建自定义UICollectionViewListCell
。
You can definitely use the same technique whenever you want to design a custom UIView
using interface builder. If you are like me, prefer to use interface builder for UI creation, then I am sure that you will find this technique quite handy.
每当您要使用界面生成器设计自定义UIView
时,都可以绝对使用相同的技术。 如果您像我一样,宁愿使用界面生成器来创建UI,那么我相信您会发现此技术非常方便。
If you like this article, feel free to follow me on Twitter.
如果您喜欢这篇文章,请随时在Twitter上关注我。
Thanks for reading and happy designing. 👨🏻💻
感谢您的阅读和愉快的设计。 💻
进一步阅读 (Further Readings)
qt自定义界面生成器