SwiftUI 初识

作为一个“不务正业”的iOS developer,这半年的时间都花在在了其它技术栈上,是时候花点时间重温一下“正业”了。

第一个想要看的就是SwiftUI了,也想试着写一点系列教程(若有不妥之处欢迎大家指正),搜索了一番,有两个比较系统的资料可供参考:

  1. SwiftUI by Tutorialswww.raywenderlich.com 出品,正版有点贵;
  2. SwiftUI与Combine编程,猫神出品必为精品。

废话不多说,开始吧(本文将以SwiftUI by Tutorials中的例子作为示例)。

环境: Xcode 11.0, macOS 10.15 (对SwiftUI支持完善)

准备

SwiftUI仅适用于iOS13,创建项目时,请选择SwiftUI,语言选择Swift,Deployment Target选择iOS 13。

Tips: Xcode 11.0 启动的项目模板中默认创建了SceneDelegate,以及在AppDelegate中仅iOS 13可使用的UISceneDelegate。若将Deployment target改为低于13的版本,需将SceneDelegate文件,AppDelegate中不可用的方法,info.plist中的不可用属性删除。同时,如果启动storyboard,需要在AppDelegate中添加 var window: UIWindow?

第一个页面

熟悉原来开发方式的我,第一个感觉是,rootViewController在哪里设置,iOS 13开始,将原来的AppDelegate进行了拆分,分出了一个名为SceneDelegate的类,至于这个类是做什么的,先不要着急,随着之后的教程会慢慢明了。

在该类中有这样一段代码:

let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
  let window = UIWindow(windowScene: windowScene)
  window.rootViewController = UIHostingController(rootView: contentView)
    self.window = window
    window.makeKeyAndVisible()
}

我们看到了对window的初始化,以及对rootViewController的赋值,赋值的对象是一个名为UIHostingController的类,看一下这个类的定义

open class UIHostingController<Content> : UIViewController where Content : View {...}

它是一个UIViewController的子类,那么它就和我们原先的开发方式并没有什么不同了。

Tips: 若想要将SwiftUI和UIKit共同使用,使用的就是这个UIHostingController。

而我们所有的UI代码就被封装在UIHostingController初始化时传入的contentView中。如果熟悉React Native,RN的初始化方式也是将一个react view赋值给了UIViewController的view属性,用于展示UI。

那么接下来,我们就一起看看这个ContentView是如何绘制UI的。

struct ContentView: View {
  var body: some View {
    Text("Match the color")
  }
}

View这个protocol中只有一个属性body,来表明UI上要绘制些什么。

public protocol View {
  associatedtype Body : View
  var body: Self.Body { get }
}

body中只能有一个根元素,这和React Native也是相同的,若想要在其中放置多个元素,则需要用一个容器将其包裹,例如 VStack,HStack(很像UIStackView)。VStack为纵向排列的容器,HStack为横向排列的容器。添加容器的方式也有两种,写代码或是从右侧的canvas中添加,canvas中添加(按住Cmd点击该元素 / 从Object Library中拖拽)有点像storyboard。看到这里我们可以发现SwiftUI两个明显优势:

  1. 声明式的UI书写方式,对比UIKit时代,当我们定义好子元素后,需要在相应的生命周期函数中添加约束,或是设置frame;
  2. 所见即所得,代码和canvas是同步的,虽然之前的@IBDesignable 和 @IBInspectable也能实现类似的效果,但远不及SwiftUI强大,便捷。在后面的view的抽离,复用上也展现出了它的不凡之处。
struct ContentView: View {
  var body: some View {
    VStack {
      HStack {
        VStack {
          Color(red: 0.5, green: 0.5, blue: 0.5)
          Text("Match the color")
        }
        VStack {
          Color(red: 0.5, green: 0.5, blue: 0.5)
          Text("R: 127  G: 127  B: 127")
        }
      }
      Button(action: {}) {
        Text("Hit me")
      }
      Slider(value: 0.5)
    }
  }
}

Tips: 所见即所得原于如下这段代码:

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

添加多个元素的过程在这里就忽略了,后面会附上GitHub链接。

双向绑定

静态页面的展示已经妥了,接下来让我们看看如何给它添加一些行为。想想原先是怎么做的。使用button.addTarget(...)或@IBAction,而在SwiftUI中使用了双向绑定的方式,这种方式也不是SwiftUI初创的了,之前原生应用可以使用RxSwift或ReactiveCocoa,React Native中可以使用hooks,比如useState(...)来实现。(猜想SwiftUI可能是借鉴了RN或是Flutter的state,YY而已,若不对请轻拍,嘿嘿)

在示例中有一个button,点击它想弹出一个Altert,如何实现呢?先看代码:

Button(action: { self.showAlert = true }) {
  Text("Hit me")
}.alert(isPresented: $showAlert) { () -> Alert in
  Alert(title: Text("Title"), message: "Hello World")
}.padding()

其中showAlert为这个ContentView的一个属性:

@State var showAlert: Bool

点击的行为就是改变showAlert的值为true,showAlert如何是alert展示出来呢?alert方法。我们来看一下他的定义:

extension View {

    /// Presents an alert.
    ///
    /// - Parameters:
    ///     - item: A `Binding` to an optional source of truth for the `Alert`.
    ///     When representing a non-nil item, the system uses `content` to
    ///     create an alert representation of the item.
    ///
    ///     If the identity changes, the system will dismiss a
    ///     currently-presented alert and replace it by a new alert.
    ///
    ///     - content: A closure returning the `Alert` to present.
    public func alert<Item>(item: Binding<Item?>, content: (Item) -> Alert) -> some View where Item : Identifiable

    /// Presents an alert.
    ///
    /// - Parameters:
    ///     - isPresented: A `Binding` to whether the `Alert` should be shown.
    ///     - content: A closure returning the `Alert` to present.
    public func alert(isPresented: Binding<Bool>, content: () -> Alert) -> some View
}

它是View的一个extension,只要是view就有这个方法,它接收两个参数,第一个就是isPresented,我们传如了$showAlert,它前面的$符号表示和参数showAlter进行绑定,绑定是什么意思呢?相当于传入了一个引用,alert方法中对showAlert的改变,将作用于@State var showAlert: Bool。第二个参数为一个closure,返回值是一个Alert对象,即原先的UIAlertView。此时这个方法的作用就很明显咯。

UI元素复用

最后我们看一下如何复用自定义的View。回忆一下UIKit时,如何复用?在storyboard或xib中定义的view基本上是很难被灵活复用的。想要复用只能用代码手写view,手写view就意味着需要定义约束或是frame,当view比较复杂时,费了🐂劲写完的view无法预览,只能让app运行起来才能看到,这样自然就拉长了反馈周期,很不便利。

SwiftUI将改变这一现状,假设我们要抽出一个如下图所示的view:


slide.png

在抽离其代码是这样的:

HStack {
  Text("0").foregroundColor(.red)
  Slider(value: .constant(0.5))
  Text("255").foregroundColor(.red)
}.padding(.horizontal)

可以通过代码直接创建一个View:

struct SlideView: View {
  @Binding var value: Double
  var textColor: Color
  var body: some View {
    HStack {
      Text("0").foregroundColor(textColor)
      Slider(value: $value)
      Text("255").foregroundColor(textColor)
    }.padding(.horizontal)
  }
}

或在canvas中按住cmd点击这个HStack,选择extract。Apple官方也推荐尽可能的抽取复用的view。这个复用的view自然也可以在canvas中看到。这个优势完美。
最后,我们来看一下效果图:

效果展示

swiftui.png

Demo 链接: GitHub

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容