List是TableView的封装么

由于我查了两天并没有找到swiftUI的开源代码,所以其内部完成我并不能断言,可是在运用过程中寻找一些蛛丝马迹,List确实与UIKit中的UITableVIew有点联络。尽管如此,正如我上一篇博客中说的那样,关于项目作为极简规划的app,swiftUI是一个十分不错的挑选。 它的极简不仅仅体现在规划上,在代码编写上,swiftUI也尽可能降低开发者关于UI自身的开发复杂度。因而,List减少了许多咱们在UITableView不愿意写,又不得不写的冗余代码,以尽可能简略的办法,完成流式布局。如下比如:

List {
  ForEach(viewModel.dataSource) { model in
    Text("(model.title)")
  }
}

这是List的一种写法,还有别的一种写法:

List(viewModel.dataSource) { model in
    Text("(model.title)")
}

详细这两种写法有什么区别,我引证一篇博客中的结论是,for-each的写法会在加载的时候将数据源数据个数的视图一次性悉数加载出来,一旦数据源数据量很大,则简略影响到性能。 现在我并没有对此进行论证,所以这儿并不给予点评。但假如各位在运用List的时候,遇到了卡顿问题,能够注意一下这两种写法。下面咱们看一下效果图:

SwiftUI开发总结(二) 大概是项目中最熟悉的面孔List

假如觉得简略,请把简略打在公屏上。还有比这更简略的布局么?假如有,那必定是安卓的ListView

事实上,咱们很少会把一个系统控件直接扔给List,因而咱们需求自定义一个视图,咱们能够起个名字,叫item或许沿用之前UIKit的传统给它起名cell,只需便于了解怎么起名都能够。如下代码:

struct MMListItem: View {
  var title: String
  var body: some View {
    return HStack {
      Text(title)
        .font(.title)
    }
    .background(Color.white)
  }
}
​
struct MMContentView: View {
  let supportedDevices: [String] =
  ["iPhone5s-iPhone5s",
   "iPadAir-iPadAir", 
   "iPadAirCellular-iPadAirCellular", 
   "iPadMiniRetina-iPadMiniRetina",
   "iPadMiniRetinaCellular-iPadMiniRetinaCellular",
   "iPhone6-iPhone6", 
   "iPhone6Plus-iPhone6Plus", 
   "iPadAir2-iPadAir2",
   "iPadAir2Cellular-iPadAir2Cellular",
   "iPadMini3-iPadMini3", 
   "iPadMini3Cellular-iPadMini3Cellular", 
   "iPodTouchSixthGen-iPodTouchSixthGen", 
   "iPhone6s-iPhone6s", 
   "iPhone6sPlus-iPhone6sPlus", 
   "iPadMini4-iPadMini4", 
   "iPadMini4Cellular-iPadMini4Cellular", 
   "iPadPro-iPadPro", 
   "iPadProCellular-iPadProCellular", 
   "iPadPro97-iPadPro97", 
   "iPadPro97Cellular-iPadPro97Cellular", 
   "iPhoneSE-iPhoneSE", 
   "iPhone7-iPhone7", 
   "iPhone7Plus-iPhone7Plus"]
 
  var body: some View {
    NavigationView {
      List {
        ForEach(0..<supportedDevices.count) {
          MMListItem(title: supportedDevices[$0])
            .listRowBackground(Color.gray)
​
        }
      }
      .listStyle(.plain)
      .background(Color.gray)
    }
  }
}
​
struct MMContentView_Previews: PreviewProvider {
  static var previews: some View {
    MMContentView()
  }
}
​
​

刚刚接触SwiftUI的人可能会一脸懵x,咱们一点一点分析,这儿我没有像他人相同从基础控件开始讲,讲一大堆的理论逻辑,仍是遵从之前的办法,整篇文档以实际操作为主,秉承着command + c and command +v即可运用的原则,赶快的将咱们带入开发节奏中。

说说什么是容器

先说容器stack,作为基础容器它的效果与安卓中的layout类极为相似,都是自身并不担任什么渲染工作,或许说最好不要把它用成一个渲染组件,仅仅担任内部视图的布局任务。

HStack为横向布局,VStack为水平布局,还有一个ZStack容器是以Z轴为布局方向布局,这儿抛出来一个问题,为什么苹果要如此规划,而不是用一个Stack+interface来操控布局办法?关于HStackVStack,同学们应该很简略具象出它们效果出来的样子,所以这儿并不多做介绍,而是引证苹果官方事例简略展示一下ZStack来了解容器概念:

let colors: [Color] =
  [.red, .orange, .yellow, .green, .blue, .purple]var body: some View {
  ZStack {
    ForEach(0..<colors.count) {
      Rectangle()
        .fill(colors[$0])
        .frame(width: 100, height: 100)
        .offset(x: CGFloat($0) * 10.0,
            y: CGFloat($0) * 10.0)
    }
  }
}

SwiftUI开发总结(二) 大概是项目中最熟悉的面孔List

假如之前有同学运用过zIndex,那么对这个概念必定不会陌生,当然有图就更简略了解了:

SwiftUI开发总结(二) 大概是项目中最熟悉的面孔List

了解一下坐标系,然后咱们接着聊容器布局。以HStack为比如,它的初始化办法能够很直观地看出它的布局办法:

  @inlinable public init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
​

VStack的初始化办法跟HStack的相同,ZStack的初始化办法中缺少了space入参。alignment是对其办法,

与安卓不同的是,Stack没有直接提供跟父视图保持一致或是依据内容填充这样的参数办法,当然这种功能并非不可能完成,能够经过GeometryReader的办法与父视图建立联络,获取父视图尺寸信息,从而依据父视图布局,本章先不做介绍。

Stack的布局办法是经过内容填充将容器撑开,假如视图深度复杂度不够(说人话的话,便是没有Stack嵌套,仅仅扁平层布局)不会呈现什么问题,可是试想一下咱们刚刚自定义itemList底色不同就会有问题。如图所示:

SwiftUI开发总结(二) 大概是项目中最熟悉的面孔List

这样显然是不符合规划要求的,由于容器巨细是其内部视图填充起来的,因而,在加载到List时,会呈现长度或许高度都可能不一致的情况,此刻就需求另一个控件的运用——space

放在箱子里的泡沫——Spacer

假设一个箱子是依据里面的货物巨细而自动弹性的,可是由于每个货物巨细不一,所以导致箱子的巨细不一致,那么,假如咱们期望箱子巨细是一致的,该怎么解决这个问题呢?最简略的办法便是往箱子里注入相同体积的泡沫,这样箱子就能够做到相同大了,那么这儿的泡沫便是Spacer

假如咱们期望每个item看起来相同长,那么就需求为其增加Spacer,扩充其内容,如下:

struct MMListItem: View {
  var title: String
  var body: some View {
    return HStack {
      Text(title)
        .font(.title)
      Spacer()
    }
    .background(Color.white)
  }
}

这样全部看起来就正常许多了。

SwiftUI开发总结(二) 大概是项目中最熟悉的面孔List

上拉下拉

接下来咱们介绍日常运用Tableview都绕不开的一个功能,上拉下拉。

SwiftUI中,系统已经帮助完成了下拉办法,运用起来也十分便利,一行代码就能够解决问题:

.refreshable {
  // 模拟数据加载时刻
  try? await Task.sleep(nanoseconds: 3_500_000_000)
}

效果如下:

SwiftUI开发总结(二) 大概是项目中最熟悉的面孔List

仍是那句话,假如不苛求app的UI规划必定是能够拿世界规划金奖的话,swiftUI是一个不错的挑选,由于它真的很简略。

说完下拉,再来说一下上拉加载更多。这儿说一个比较取巧的办法,便是利用视图的onAppear办法,来触发加载逻辑。

完成逻辑是将自定义视图放在List队尾,一旦自定义视图被展示出来,则触发加载动画,进行加载,这儿我将我自定义的footView贴在下面,有需求的能够自取,然后删删改改:

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
final class XXRefreshHandler {
  var moreAction: (() async -> Void)?
}
@available(iOS 13.0, macOS 10.15, *)
struct XXActivityIndicator: UIViewRepresentable {
​
  @Binding var isAnimating: Bool
  let style: UIActivityIndicatorView.Stylefunc makeUIView(context: UIViewRepresentableContext<XXActivityIndicator>) -> UIActivityIndicatorView {
    return UIActivityIndicatorView(style: style)
  }
​
  func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<XXActivityIndicator>) {
    isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
  }
}
​
struct XXRefreshFooterView: View {
  @Binding var noMoreData: Bool
  var footHandler = XXRefreshHandler()
  
  public func getMoreAction(action: @escaping () async -> Void) -> XXRefreshFooterView {
    self.footHandler.moreAction = action
    return self
  }
  
  var body: some View {
    return HStack(alignment: .center) {
      Spacer()
      if noMoreData {
        Text("No More Data")
      } else {
        XXActivityIndicator(isAnimating: .constant(true), style: .medium)
        Text("Loading..")
      }
      Spacer()
    }
    .background(Color.clear)
    .onAppear {
      Task {
        // 模拟网络恳求
        try? await Task.sleep(nanoseconds: 3_500_000_000)
        await footHandler.moreAction?()
      }
    }
    .listRowSeparator(.hidden)
​
  }
}
​

假如关于协程还不清楚的同学,能够先疏忽Task中的代码,以及删去asyncawait等关键字。

XXActivityIndicator: 俗称‘菊花轮’,是常用的等候指示器,上面我将其封装好,假如需求自定义样式,能够自行修正。

XXRefreshHandler: 需求注意一下这个类,之所以选用这样的一种写法,出于两方面原因:

  1. XXRefreshFooterView作为结构体,其成员是在初始化之后不能被直接修正的。
  1. swiftUI 的body内部不允许有数据的操作(能够经过闭包的办法封装数据处理逻辑,但结果必须回来view实例)。

因而这儿运用class将特点进行了包装,这儿还能够选用以下办法修正:

    public func getMoreAction(action: @escaping () async -> Void) -> XXRefreshFooterView {
    		var result = self
        result.moreAction = action
        return result
    }

结语

这篇跟咱们大概了聊了一下怎么运用swiftUIlist进行布局,以及一些常用办法的总结,期望能够帮助同学们快速上手swiftUI

以我个人的开发经验而言,万事开头难,但只要上手之后,才能发现问题,解决问题,这些问题跟空想或许单纯学习理论知识是不相同的。真实投入开发中会发现,许多问题是细枝末节,想要处理好也需求很大功夫。因而,仍是期望咱们着手先试一下,有什么问题沟通评论,请在下面留言。