SwiftUI 实战:从 0 到 1 研发一个 App

Easter79
• 阅读 1130

心得感悟

起初看到 WWDC 上的演示 SwiftUI 时,我就觉得 SwiftUI 有种陌生的熟悉感(声明式语法),所以体验下,看看有没有什么启发。

先说下整体项目完成下来的感受:

  • 用 Swift + SwiftUI 开发 iOS 项目效率很高,本人之前没有接触过 Swift 语言,这次是从 0 开始学 swift 语言以及 swiftUI 框架的,每天花 2 个小时断断续续大致花了 3 天时间掌握了基本的 Swift 语法,而 SwiftUI 框架的掌握是按照官方的视频学习的(赞下 Apple 的文档、教学视频的完备性);
  • 从我一个前端开发工程师的视角看来,SwiftUI 使用的 “DSL”、状态管理和前端的 React 很相似,不少概念是相通的,比如 State,Props。
  • SwiftUI + Xcode11 + macOS Catalina 让研发效率大大提升,预览模式做到了所见即所得,注意这里是预览模式而不是模拟器模式
  • 借助于预览模式,UI 也可以做到很好的单页面的单元测试;这一点比较有意思,独立的 View 可以独立预览,然后可以注入数据,实时预览调试。

SwiftUI 实战:从 0 到 1 研发一个 App

执行环境

  • macOS Mojave: 10.14.5
  • xcode: Version 11.0 beta 6 (11M392q)

项目信息

github: https://github.com/young-cowboy/swiftui-app-habits

App 效果预览

SwiftUI 实战:从 0 到 1 研发一个 App

SwiftUI 实战:从 0 到 1 研发一个 App

2 个关键点

这里提 2 个关键点“尾部闭包语法”以及“状态管理”,因为理解他们就基本能掌握 SwiftUI 来开发一个简单的项目。尾部闭包语法能让我们理解 SwiftUI 是如何通过声明式语法来创建 View 的,“数据传递”能让我们了解怎么实现数据流功能。

尾部闭包语法

Swift 有一个很重要的概念叫做“尾部闭包语法”,这种语法让 Swift DSL 的书写变得更“声明式”,我们先了解下 Swift 闭包定义:

{(parameters) -> return type in
  // 代码
}

这样来看是不是和 JavaScript 的闭包有一定的相似,但是闭包有很多种语法形式,这里我们举个例子说明下:

import Foundation

func wrapClosure (closure: (String, String) -> Void) {
    let name = "KK"
    let age = "18"

    closure(name, age)
}

定义一个 function,参数只有一个,并且这个参数为一个函数,然后在方法体内部调用这个函数,传入参数;

func foo (name: String, age: String) {
    print("My name is \(name) and \(age) years old")
}

wrapClosure(closure: foo) // My name is KK and 18 years old

通常我们会这样执行,定义个函数来处理传参。但是利用闭包的语法,有更简洁的方式,如下就一个闭包的语法了

wrapClosure(closure: { (name: String, age: String) in
    print("My name is \(name) and \(age) years old")
})

还可以利用 Swift 的类型推断能力,不写闭包参数的类型,如下

wrapClosure(closure: { name, age in
    print("My name is \(name) and \(age) years old")
})

还可以利用快捷参数名来获取参数,如下

wrapClosure(closure: {
    print("My name is \($0) and \($1) years old")
})

最后还有更简洁的方式,这样是 SwiftUI 利用到的一个特性,所以SwiftUI 的 DSL 看起来很有声明式的感觉,这个特性叫 “尾部闭包语法”,这次方法执行连括号都可以不写了。

如果一个闭包是以一个函数的最后一个参数传递的,那么它就可以在函数的圆括号以外内联。

wrapClosure {
    print("My name is \($0) and \($1) years old")
}

到此应该能看出为什么 SwiftUI 能用声明式的语法来创建各种 View 协议的视图了。

数据传递

在做面向用户的功能时,一个很主要的点是数据传递,不同的设计理念解决特点场景的问题。这里简单介绍下 SwiftUI 的数据怎样传递,这里有一个概念property wrapper,我理解是装饰器 decorate,介绍三个装饰器 @State@Binding@ObservedObject@Published

  • @State 装饰过的属性发生了变化,SwiftUI 会根据新的属性值重新创建视图
  • @Binding 修饰器修饰后,属性变成了一个引用类型,传递变成了引用传递,这样父子视图的状态就能关联起
  • @ObservedObject 修饰一个复杂类型数据,可以被多个 View 所使用,用 @Published 修饰对象里属性,表示需要被监听;

更多的细节可以参考:https://mecid.github.io/2019/06/12/understanding-property-wrappers-in-swiftui/

页面结构设计

SwiftUI 实战:从 0 到 1 研发一个 App

一共有 4 个页面 HabitListView, AddButtonView, HabitDetailView, AddItemView

数据结构设计

项目涉及到三个数据结构: HabitItem(习惯项), HabitIconArray(习惯图标), HabitColor(习惯主题)

HabitColor 主题色,保存了习惯的主题

public let UserColorArray = [
  Color(red:75 / 255, green:166 / 255, blue: 239 / 255),
  Color(red:161 / 255, green:206 / 255, blue: 97 / 255),
  Color(red:248 / 255, green:214 / 255, blue: 80 / 255),
  Color(red:243 / 255, green:176 / 255, blue: 74 / 255),
  Color(red:238 / 255, green:140 / 255, blue: 111 / 255),
  Color(red:237 / 255, green:113 / 255, blue: 165 / 255),
  Color(red:207 / 255, green:102 / 255, blue: 247 / 255),
  Color(red:77 / 255, green:110 / 255, blue: 247 / 255),
  Color(red:236 / 255, green:107 / 255, blue: 102 / 255)
]

HabitIconArray 习惯的图标库

public let IconNameArray: [String] = [
    "alarm",
    "book",
    "pencil",
    "desktopcomputer",
    "gamecontroller",
    "sportscourt",
    "lightbulb"
]

HabitItem 用来保存习惯的详细信息,这里实现 ObservableObject 协议,用来告诉 SwiftUI 这个对象需要监听,用 Published property wrapper 包装了 checkList 属性,表示这个属性是要监听的,因为它可能需要传递给子 View

class HabitItem: Identifiable, ObservableObject {
    var name: String = ""
    var iconName: String = "clock"
    var theme: Color = UserColor.color1.value
    var uuid: Int = 0

    @Published var checkList: [Bool] = [false, false, false, false, false, false, false]
    
    init () {
        
    }
    
    init(name: String, iconName: String, theme: Color) {
        self.name = name
        self.iconName = iconName
        self.theme = theme
        self.uuid = generatteID()
    }
}

UI 设置

具体代码参考仓库代码,这里讲解一个流程,新建一个“习惯”项,在 MainView 里

MainView

  ...
  @State var sheetVisible: Bool = false
  @State var sheetType: String = "add"
  ...
  ...
  AddButtonView() {
      self.sheetType = "add"
      self.sheetVisible = true
  }
  ...

AddButtonView 利用尾部闭包语法内联了一个闭包用来相应事件

AddButtonView 里定义了 onPressed 属性

struct AddButtonView: View {
    var onPressed: () -> Void
    
    var body: some View {
        Button(action: {
            self.onPressed()
        }) {
            HStack {
                Image(systemName: "plus.circle.fill")
                    .resizable()
                    .frame(width: 60, height: 60)
                    .foregroundColor(Color.blue)
            }
        }
    }
}

AddItemView 里的新增按钮点击后相应事件,把选中的数据传递 onSumit 属性回调里

struct AddItemView: View {
    @State var newItemTitle = ""
    @State var selectIconIndex: Int = 0
    @State var selectColorIndex: Int = 0
    
    var onSumit: (HabitItem) -> Void
    var onDissmis: () -> Void
    
    var body: some View {
         VStack {
            ...
            ...
            VStack {
                Button(action: {
                    if self.newItemTitle != "" {
                        let iconName = IconNameArray[self.selectIconIndex];
                        let theme = UserColorArray[self.selectColorIndex];

                        self.onSumit(HabitItem(name: self.newItemTitle, iconName: iconName, theme: theme))
                    }
                }) {
                    Text("新增").frame(minWidth: 0, maxWidth: .infinity)
                }
                ...
              }
            ...
            }
          ...
        }
    }
}

在 MainView 里,利用闭包处理回调事件新增选项

AddItemView(onSumit: { item in
    self.habitItemList.insert(item, at: 0)
    self.sheetVisible = false
  }, onDissmis: { self.sheetVisible = false })

剩下的功能大同小异,可以把代码拉下来本地运行看看效果,github: https://github.com/young-cowboy/swiftui-app-habits

谢谢

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
Wesley13 Wesley13
3年前
35岁是技术人的天花板吗?
35岁是技术人的天花板吗?我非常不认同“35岁现象”,人类没有那么脆弱,人类的智力不会说是35岁之后就停止发展,更不是说35岁之后就没有机会了。马云35岁还在教书,任正非35岁还在工厂上班。为什么技术人员到35岁就应该退役了呢?所以35岁根本就不是一个问题,我今年已经37岁了,我发现我才刚刚找到自己的节奏,刚刚上路。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k