65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIconemptyStarIcon

1.40/5 (3投票s)

2023年2月22日

CPOL

3分钟阅读

viewsIcon

8302

通过一个简单的例子展示了如何实现 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

创建一个名为 RecipeItemViewXib 文件

打开 Xib 文件并根据图片进行更新

存在两个同名的文件,分别具有 SwiftXib 扩展名。

但是 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 文件获取视图类。

  1. Nib 文件内容下载为数组。 Xib 文件可以有一个或多个 UIView 甚至 UIViewController 元素。要获取它们,请调用 loadNibNamed
    let elementsOfNib = Bundle.main.loadNibNamed
                        ("RecipeItemView", owner: nil, options: nil)

    该函数返回可选值。要从数组中获取相应的视图,请使用索引或类似的方法

    let recipesItemView = elementsOfNib?.first

    recipesItemViewRecipesItemView 类的可选实例,具有已初始化的出口(outlets)。

  2. 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)

这里的 selfRecipesViewController。当此函数返回时,变量 itemView 将自动获取一个值。它是 RecipeItemView 的实例。

3. 如何使 Table View 单元格自适应大小?

RecipeItemViewtitleLabelimageViewdescriptionLabelHeight 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 订阅它。 ViewViewModel 发送消息,以生成新的食谱数据,然后获取数据并更新 tableView。结果,您应该看到类似这样的内容

源代码

历史

  • 2023年2月17日:初始版本
  • 2023年2月21日:添加了 SwiftUI 版本的自适应大小 table view
© . All rights reserved.