给SwiftUI找个替补 -- Lima

1,850 阅读6分钟

背景

今年的WWDC2019上最令人激动的新技术无疑就是全新的UI框架SwiftUI了。对于苹果开发者来说,它为大家开启了一个美丽的新世界:

  • 简洁的声明式语法。极致简洁易读,百行代码变十行。
  • 统一苹果旗下所有平台。iOS,macOS,tvOS, watchOS。
  • 实时预览,多设备同时预览。修改代码或预览都可以动态生效。
  • 清晰的数据流管理。结合Combine框架实现响应式编程。

SwiftUI着实惊艳,但遗憾的是它要求iOS 13.0以上的系统。这一条约束让大家觉得它还很遥远,因为国内多数App还要适配iOS 9.0,甚至iOS 8.0。

开源声明式框架

其实,SwiftUI的核心在于声明式语法描述界面,而声明式语法并不是什么新鲜事物,著名的React和近来大热的Flutter都采用了声明式语法。笔者查找和比较了一些开源的声明式界面框架,希望找到一个SwiftUI的合适替补,让大家在自己现有的项目里也能拥有类似SwiftUI的开发体验。

Layout
国外大神出品,基于自定义的XML DSL语法,功能强大,完备,支持Live Reload,自带工具集。

TemplateKit
类React框架,基于自定义的XML DSL语法,支持Live Reload。

LayoutKit
LinkedIn出品,质量性能放心。

ComponentKit
Facebook出品,使用Objective-C++编写,异步高性能布局,支持组件回收重用,优化内存和滑动性能。

Render
类React框架,支持组件回收重用,优化内存和滑动性能。

Tokamak
类React框架。

Komponents 类React框架,声明式语法比较简洁,与SwiftUI较为接近。

Few
类React框架。

Lima
超轻量级的框架,语法上与SwiftUI最为接近。

方案选择

这些框架都是在苹果发布SwiftUI之前诞生的。比较完一圈以后,我不由得感叹,还是SwiftUI更优雅,更强大。这也不难理解,毕竟苹果是后来者,SwiftUI是汲取了业界最新理念,厚积薄发的超越之作。

至于哪个框架更值得推荐,我想应该是因人而异。熟悉React的朋友,可以选择类React框架;看重性能的朋友,可以选择两个大厂的框架;需要实时预览开发体验的朋友,可以考虑前两个框架。而从我个人来说,我选择『小而美』的Lima

它的优势如下:

  • 超轻量,源码十分简单易懂,方便自行扩展。
  • 语法上与SwiftUI最为接近,简洁易懂。
  • 完全基于UIKit现有控件,没有复杂的概念。

我们来做个比较,看下Lima和SwiftUI实现下面的界面,它们的代码各长得什么样:

SwiftUI

HStack {
   VStack {
       Text("★★★★★")
       Text("5 stars")
   }
   VStack {
       HStack {
           Text("Avocado Toast").font(.title)
           Spacer()
           Image("20x20_avocado")
       }
       Text("Ingredients: Avocado, Almond Butter, Bread, Red Pepper Flakes").lineLimit(1)
   }
}

Lima

LMRowView(spacing: 10,
   LMColumnView(
       UILabel(text: "★★★★★", textAlignment: .center),
       UILabel(text: "5 stars", textAlignment: .center)
   ),
   LMColumnView(
       LMRowView(
           UILabel(text: "Avocado Toast", font:.preferredFont(forTextStyle: .title1)),
           LMSpacer(),
           UIImageView(image: UIImage(named: "20x20_avocado"))
       ),
       UILabel(text: "Ingredients: Avocado, Almond Butter, Bread, Red Pepper Flakes")
   )
)

看起来是不是很像,而且还是我们熟悉的UIKit,简直完美了。

Lima介绍

适用性

建议不要通过pod集成Lima,因为podspec上的Swift version和Deployment Target并不准确。实际上它的适用性是挺广泛的,可以支持iOS 9.0以上系统,稍作修改,也能支持iOS 8.x。而且Swift 4.x和Swift 5.x都可以编译通过。 另外,由于源码里有Objective-C的代码,所以纯Swift的项目最好是集成编译好的Lima.framework。

框架说明

通过上面的例子我们也看到,Lima本身很简单,很容易上手。这点和Masonry很像,你甚至无需预先学习AutoLayout,只是通过看几个例子就可以开始使用Masonry了。Lima也是如此一目了然,你也无需去学习SwiftUI,花两个小时看一下项目首页上的那个说明文档,跑一下Demo,基本就能开始编码了。

这里,我给大家简单理一下Lima的源码。

  • Lima是对UIKit的封装,内部通过AutoLayout实现布局。

  • LMLayoutViewLMBoxView是容器类的抽象基类,并不直接调用。

  • LMRowViewLMColumnViewLMAnchorViewLMRootView是四个容器类。

    • LMRowView是水平容器;
    • LMColumnView是垂直容器;
    • LMAnchorView用于将子视图排布在它的边缘;
    • LMRootView通常用于控制器的根视图,绕过系统定义的视图Margins。
  • LMSpacer默认是当做空白,同时也可以当做分隔线。

    LMSpacer(height: 0.5, backgroundColor: UIColor.gray)
    
  • LMScrollViewLMTableViewCellLMTableViewHeaderFooterViewLMCollectionViewCell是UIKit相应类型的子类。它们都有一个content属性,用于接收内容视图,而且它们的大小自动由content视图决定。

  • 对于常用的UIKit控件和上述Lima定义的类型,Lima都在扩展中给它们定义了便利构造器,我们在声明式语法中需要通过这些构造器声明控件。

  • 每个构造器的最后一个参数(with)都是一个参数为自身的闭包。这让我们可以在尾随闭包里获取控件的实例,并设置更多的属性。

    var detailView: LMColumnView!
    LMColumnView(margin: 8, verticalAlignment: .top, spacing: 0) {
       self.detailView = $0
       $0.clipsToBounds = true
    }
    
  • 除了构造器,我们还需要关注头文件里定义的属性。视图的排布是通过设置这些属性实现的。

My Fork

建议有兴趣尝试Lima的朋友,使用笔者fork的源码。

Lima

我这份源码修改了一个可能的同名属性覆盖问题,并且对原始代码做了一些扩展。

  • Lima在UIView的分类里定义了width和height属性。这两个属性名很容易导致同名属性覆盖的问题。我们常用的YYKit里就定义了这两个属性。所以,我给这两个属性加了lm_前缀。

  • 我给LMColumnView, LMRowViewLMAnchorView加了一个可以接收子视图数组的构造器,这样可以实现如下的调用:

    let items = ["First", "Second", "Third", "Four", "Five"]
    LMColumnView(
       items.map { item in
           UILabel(text: "Hello, \(item)!", textAlignment: .center)
       }
    )
    
  • 我给UIButtonUITextField的构造器里加入了事件处理参数,方便调用:

    UIButton(title: "Press Me!", action: { [weak self] btn in
       self?.showGreeting()
    })
    
  • 我添加了一个LMTableView类,期望实现SwiftUI里简单的List效果。

    SwiftUI

    List(landmarks) { landmark in
       HStack {
          Image(landmark.thumbnail)
          Text(landmark.name)
          Spacer()
          
          if landmark.isFavorite {
             Image(systemName: "star.fill")
                .foregroundColor(.yellow)
          }
       }
    }
    

    Lima

    LMTableView(items, { item in
       LMTableViewCell(
           LMRowView(
               UILabel(text: "Hello, \(item)!"),
               LMSpacer(),
               UIImageView(image: UIImage(named: "EmailIcon")) {
                   if item == "Five" {
                       $0.isDisplayable = false
                   } else {
                       $0.isDisplayable = true
                   }
               }
           )
        )
    })
    

    不过,我的实现Cell没有做到重用,只适用于短列表。也欢迎大家继续研究。

不足

Lima用简单的代码实现了接近SwiftUI的简洁的声明式语法。但是,它只能看做是一个简化版的SwiftUI,远没有SwiftUI强大。

  • 没有SwiftUI那样漂亮的状态和数据流实现。
  • 没有SwiftUI那样强大简洁的列表实现。
  • 没有SwiftUI那样的动画和绘图实现。
  • 容器类会引入多余的视图层级,不太适合需要性能或内存利用率的场景。

总结

SwiftUI很强大,很美丽,但是我们还用不起来它。所以,笔者希望找一个开源的声明式UI框架,在现有的项目里引入声明式编程。笔者找到了Lima,并扩展了它。如果,你现在正在用Masonry布局UI,我建议您不妨试试Lima,因为声明式UI是未来的趋势,而且Lima和Masonry一样简单易用,Lima比Masonry的代码更加简洁易读。

Lima