iOS Cocoa 布局 - Nib 和 Xib 之间的区别






1.40/5 (3投票s)
通过一个简单的例子展示了如何实现 UIKit 和 SwiftUI 中 TableView 的自适应大小单元格,并解释了 Nib 和 Xib 之间的区别,如何加载 nib 文件以及 load nib file 方法中 owner 参数的具体含义。
通过一个名为 MyRecipes
的简单示例,讲解一些面试中可能会问到的问题,您可以在其中管理您最喜欢的食谱。
1. Xib 和 Nib 文件有什么区别?
让我们创建一个新的 Xcode
项目,命名为 MyRecipes
进入项目导航器。将 ViewController
重命名为 RecipesViewController
。打开 main.storyboard
。选择 ViewController
。在 Identity Inspector(身份检查器)中选择 RecipesViewController
。添加 UITableView
到视图控制器。确保 table view 只有一个 Prototype Cell(原型单元格)。我们称之为“cell
”
向项目中添加一个新的 Cocoa Touch Class,命名为 RecipeItemView
创建一个名为 RecipeItemView
的 Xib 文件
打开 Xib 文件并根据图片进行更新
存在两个同名的文件,分别具有 Swift 和 Xib 扩展名。
但是 Nib 文件在哪里呢?
Nib 文件是 Xcode 在编译 Xib 文件期间创建的文件。
Nib 具有一些通用格式,用于在运行时恢复视图。
Xcode 将其保存在已构建的应用程序文件夹中。打开一个类似于此路径的文件夹
˜/Library/Developer/Xcode/DerivedData/MyRecipes-…./Build/Products/Debug-iphonesimulator/MyRecipes
然后打开上下文菜单(通过鼠标右键单击)并选择 Show Package Contents(显示包内容)
在图片中,您可以看到 Nib 文件。就在这里!
2. 如何使用 Xib 文件?
Xib 与用户界面相关联。在我们的例子中,RecipesItemView.xib 与 RecipesItemView UIView
类相关联。
我们将使用该视图作为 UITableViewCell
的内容视图。因此,我们需要下载 Nib,将其解析为 RecipesItemView
类,并将其作为子视图添加到单元格的 Content view(内容视图)。
有两种方法可以从 Nib 文件获取视图类。
- 将 Nib 文件内容下载为数组。 Xib 文件可以有一个或多个
UIView
甚至UIViewController
元素。要获取它们,请调用loadNibNamed
let elementsOfNib = Bundle.main.loadNibNamed ("RecipeItemView", owner: nil, options: nil)
该函数返回可选值。要从数组中获取相应的视图,请使用索引或类似的方法
let recipesItemView = elementsOfNib?.first
recipesItemView
是RecipesItemView
类的可选实例,具有已初始化的出口(outlets)。 - 从 Nib 文件获取元素的另一种方法是使用
owner
参数调用loadNibNamed
Bundle.main.loadNibNamed("RecipeItemView", owner: self, options: nil)
这里的 self
参数是什么,为什么我们不将结果分配给任何东西?让我们逐步考虑。
打开 RecipeItemView.xib 文件并将 Placeholders(占位符) — File’s Owner(文件所有者) 指定为 RecipesViewController
打开 RecipesViewController.swift
。添加 @IBOutlet var
class RecipesViewController: UIViewController {
…
@IBOutlet var itemView: RecipeItemView!
让我们回到 Xib 编辑屏幕。选择 File’s Owner(文件所有者)。按住鼠标右键。拖动到 RecipesItemView
。
从上下文菜单中选择 itemView
。从现在开始,我们将 File owner(文件所有者) 和 Xib 的元素绑定在一起。
让我们回到 loadNibNamed
Bundle.main.loadNibNamed("RecipeItemView", owner: self, options: nil)
这里的 self
是 RecipesViewController
。当此函数返回时,变量 itemView
将自动获取一个值。它是 RecipeItemView
的实例。
3. 如何使 Table View 单元格自适应大小?
将 RecipeItemView
的 titleLabel
、imageView
和 descriptionLabel
的 Height Constraints(高度约束) 与 NSLayoutConstraints
出口(outlets)绑定
打开 RecipesViewController
。从 viewDidLoad
方法配置 UITableView
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath) {
cell.layoutIfNeeded()
}
打开 RecipeItemView.swift
。计算 height
约束
func configure(with cell: UITableViewCell, model: ReceipeData?, width: CGFloat) {
guard let model else { return }
titleLabel.text = model.title
imageView.image = model.image
descriptionLabel.text = model.description
let h1 = model.title?.height(for: width, font: titleLabel.font) ?? 0
let h2 = model.image?.height(for: width) ?? 0
let h3 = model.description?.height(for: width, font: descriptionLabel.font) ?? 0
titleLabelHeight.constant = h1
imageViewHeight.constant = h2
descriptionLabelHeight.constant = h3
let height: CGFloat = h1 + h2 + h3 + 5.0 * 4
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
leadingAnchor.constraint(equalTo: cell.leadingAnchor),
trailingAnchor.constraint(equalTo: cell.trailingAnchor),
topAnchor.constraint(equalTo: cell.topAnchor),
bottomAnchor.constraint(equalTo: cell.bottomAnchor),
heightAnchor.constraint(equalToConstant: height)
])
}
下一步也是最后一步是准备 UITableViewCell
。覆盖 UITableViewDataSource
委托的两个方法
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewModel.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) ->
UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
Bundle.main.loadNibNamed("RecipeItemView", owner: self, options: nil)
// itemView is laded now. We can use it.
cell.addSubview(itemView!)
itemView!.configure(with: cell, model: viewModel[indexPath.row],
width: tableView.frame.width)
return cell
}
table view 单元格应该根据内容更改其高度。
让我们考虑一下 SwiftUI
的相同答案。创建一个新的 Xcode 项目,界面为 SwiftUI
。将主内容视图替换为以下代码
struct RecipesContentView: View {
@EnvironmentObject var viewModel: RecipesFakeData
var body: some View {
GeometryReader { geometry in
VStack {
NavigationBar()
List {
ForEach(0..<viewModel.count, id: \.self) { index in
RecipeRow(index: index, width: geometry.size.width - 30.0)
.frame(width: geometry.size.width - 30.0,
height: rowHeight(index: index,
width: geometry.size.width - 30.0))
.background(.black)
}
}
.background(.black)
.scrollContentBackground(.hidden)
}
.background(.black)
}
}
private func rowHeight(index: Int, width: CGFloat) -> CGFloat {
if let model = viewModel[index],
let view = Bundle.main.loadNibNamed("RecipeItemView",
owner: nil, options: nil)?.first as? RecipeItemView {
var height = model.title?.height(for: width,
font: view.titleLabel.font) ?? 0
height += model.image?.height(for: width) ?? 0
height += model.description?.height(for: width,
font: view.descriptionLabel.font) ?? 0
return height + 5.0 * 3 // see RecipeItemView.xib layout
}
return 0.0
}
}
struct RecipeRow: UIViewRepresentable {
@EnvironmentObject var viewModel: RecipesFakeData
typealias UIViewType = RecipeItemView
private let index: Int
private let width: CGFloat
init(index: Int, width: CGFloat) {
self.index = index
self.width = width
}
func makeUIView(context: Context) -> RecipeItemView {
if let model = viewModel[index],
let view = Bundle.main.loadNibNamed
("RecipeItemView", owner: nil, options: nil)?.first as? RecipeItemView {
view.titleLabel.text = model.title ?? ""
view.imageView.image = model.image
view.descriptionLabel.text = model.description ?? ""
view.titleLabelHeight.constant = model.title?.height
(for: width, font: view.titleLabel.font) ?? 0
view.imageViewHeight.constant = model.image?.height(for: width) ?? 0
view.descriptionLabelHeight.constant = model.description?.height
(for: width, font: view.descriptionLabel.font) ?? 0
return view
}
return RecipeItemView()
}
}
正如您现在看到的,我们使用 loadNibNamed
方法,其中 owner = nil
。它返回一个包含视图组件的数组。
结论
MyRecipes
应用程序是使用 MVVM 模式开发的。 ViewModel
使用 Combine PassthroughSubject 发布数据,并且 View 订阅它。 View 向 ViewModel
发送消息,以生成新的食谱数据,然后获取数据并更新 tableView
。结果,您应该看到类似这样的内容
源代码
历史
- 2023年2月17日:初始版本
- 2023年2月21日:添加了 SwiftUI 版本的自适应大小 table view