苹果近期推出了一个全新的 SwiftUI 框架,可以极大地提高 iOS 上 UI 界面的开发效率。今天小彭就用 SwiftUI 来实现一个新闻资讯 app,看看能有多快。
受篇幅所限,我们将通过上下两篇文章为大家介绍如何实现一个完整的新闻资讯 app,本篇主要内容有:
- SwiftUI 的基础知识:预览、View 协议、修饰器、@State 特性等。
- 使用 NavigationView、NavigationLink、List、Text、Image 等基本视图和 HStack、VStack 常用的布局方式创建资讯列表页、资讯详情页并显示从 daliy.plist 文件读取的新闻信息。
开始
在 Xcode 11,创建一个新项目,选择 iOS->Single View APP,命名为 Daily,并选中 Use SwiftUI。
ScenDelegate
SwiftUI 项目新增了一个 ScenDelegate.swift 文件,AppDelegate 和 ScenDelegate 共同用于管理 app 的生命周期。ScenDelegate 的 scene(_:willConnectTo:options:) 进行 UI 配置:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
预览(Preview)
打开 ContentView.swift 文件,右上角有一个 Resume 按钮,如果没有看见 Resume 按钮,选择 Editor > Canvas。
点击 Resume 对 ContentView 视图进行预览
要支持预览视图,需要实现 PreviewProvider 协议,并在协议属性内返回该视图实例,比如为 ContentView 提供预览:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
以往写一个页面,如果这个页面藏得很深,那么你运行后进入到这个页面需要耗费很长时间。现在不需要运行,通过预览可以马上看到页面效果,节省了页面开发时间。
注意:必须 macOS 10.15 以上系统才支持预览功能!
View 协议和修饰器
ContentView 实现了 View 协议。所有视图都需要实现 View 协议,在协议属性 body 内提供视图内容和布局。
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
Text 也是一个 View。注意:自定义视图的 body 最多创建 10 个子视图。
通过修饰器(modifier)来配置视图,比如将 Text 的文字设置为红色,字体为 system,大小为 16,那么可以通过 foregroundColor、font 修饰器来配置:
Text("Hello World")
.foregroundColor(.red)
.font(.system(size: 16))
每个修饰器都会返回新的视图 View,通过多个修饰器形成一个修饰链来完成对适配的配置。需要注意的是,修饰器的顺序不同可能产生不一样的结果。
数据准备
每一条新闻资讯包含标题、作者、发布日期、详细内容、图片等信息。创建一个 swift 文件,命名为 News.swift。在该文件内创建一个新闻模型 News:
struct News: Identifiable {
var id: String
var author: String
var title: String
var date: Date
var content: String
var thumbnail: String
}
为了简单起见,预定义了 2 条新闻资讯信息。fetchDaliy() 方法获取新闻信息并返回 News 数组:
func fetchDaliy() -> [News] {
let newsDicts = [
["id": "0", "title": "60 天打印一枚火箭", "author": "ifanr", "content": "今年 4 月,SpaceX 重型猎鹰完成首次商业发射", "date": "10-14"],
["id": "1", "title": "当你在看电视的时候,电视也在看你", "author": "ifanr", "content": "智能电视广告的问题着实让人心烦。", "date": "10-15"]]
var newsList: [News] = []
for newsDict in newsDicts {
let news = News(id: newsDict["id"]!, title: newsDict["title"]!, author: newsDict["author"]!, date: newsDict["date"]!, content: newsDict["content"]!, thumbnail: newsDict["thumbnail"]!)
newsList.append(news)
}
return newsList
}
UI 构建
App 的主要内容有:新闻列表页、新闻详情页。我们将所有新闻资讯展示在一个列表上,每一行代表一条新闻,点击一行进入到资讯详情页。相应地,需要三个页面:列表页、列表项、详情页。新建三个 SwiftUI 文件,分别命名为 NewsListView.swift、NewsListRow.swift、NewsDetailView.swift 。NewListView 表示列表页,NewsListRow 表示列表项,NewsDetailView 表示详情页。
页面导航
在列表页点击一条新闻,将跳转到详情页,同时详情页也能够返回到列表页。在 SwiftUI 中,使用 NavigationView 和 NavigationLink 来实现页面之间的导航跳转。将列表页 NewsListView 嵌入到 NavigationView 中,SwiftUI 会在 NewsListView 顶部增加一个导航栏 NavigationBar。打开 ContentView.swift 文件,增加以下代码:
struct ContentView: View {
var body: some View {
NavigationView {
NewsListView()
}.navigationViewStyle(StackNavigationViewStyle())
}
}
navigationViewStyle 指定导航类型,若未指定,在 iPhone 和 iPad 默认的导航类型不一样。
新闻列表页
在 NewsListView 中,我们需要获取所有新闻资讯数据,并展示在一个列表上。
通过 List 声明一个列表,并指定数据源和每一行的内容。
struct NewsListView: View {
var newsList: [News] = fetchDaliy()
var body: some View {
List(newsList) { (news) in
NavigationLink(destination: NewsDetailView()) {
NewsListRow()
}
}
}
}
NavigationLink 通过指定目标页面,实现页面之间的跳转。
另外需要注意的是,List 的初始化方法指定了数据源,数据源的元素需要实现 Identifiable,因此我们在 News 类型实现了 Identifiable。
新闻列表项
每一个列表项显示一条新闻,内容包含图片、标题、发布日期。布局如下图:
在 SwiftUI 中,使用 HStack 和 VStack 来组合不同的使用的视图。HStack 表示横向关系,VStack 表示纵向关系。标题和日期是纵向关系,而标题和日期组合起来和图片是横向关系。NewListRow 的实现如下面代码:
struct NewsListRow: View {
var news: News
var body: some View {
HStack {
WebImageView(imageUrl: news.thumbnail)
.frame(width: 80, height: 100)
VStack(alignment: .leading) {
Text(news.title)
.font(.headline)
.padding(.top, 10)
Spacer()
Text(news.date)
.foregroundColor(.secondary)
.padding(.bottom, 10)
}
.padding(.leading, 10)
}
}
}
我们需要实现 WebImageView 获取网络图片并显示,图片未下载时显示一个占位图片,当图片下载后将显示该网络图片:
struct WebImageView: View {
@State private var uiImage: UIImage? = nil
let placeholderImage = UIImage(named: "mine_ifanr")
var imageUrl: String
var body: some View {
Image(uiImage: self.uiImage ?? placeholderImage!)
.resizable()
.onAppear(perform: downloadWebImage)
}
func downloadWebImage() {
guard let url = URL(string: imageUrl) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data, let image = UIImage(data: data) {
self.uiImage = image
} else {
print("error: \(String(describing: error))")
}
}.resume()
}
}
Image 是 SwiftUI 的图片控件,uiImage 为 nil 时,Image 视图显示 placeholderImage 占位图。当 WebImageView 出现时(onAppear),将会根据 imageUrl 下载图片,下载完成后将 image 赋值给 uiImage,此时 SwiftUI 会自动更新 Image 的图片。Image 能够自动更新,是因为将 uiImage 声明了 @State 特性。将 View 的存储属性声明为 @State 后,每当该属性的值发生变化时,SwiftUI 都会重新绘制 body 的内容。
新闻详情页
在新闻详情页,我们需要显示标题、作者、发布日期、详细内容。布局如图:
代码如下所示
struct NewsDetailView: View {
var news: News
var body: some View {
VStack {
Text(news.title)
.font(.system(size: 24))
HStack {
Text("作者: \(news.author)")
Text(news.date)
}.padding(.top, 2)
Text(news.content)
.padding(.top, 5)
Spacer()
}.navigationBarTitle("详情", displayMode: .inline)
.padding(EdgeInsets(top: 10, leading: 10, bottom: 0, trailing: 10))
}
}
通过 navigationBarTitle 来设置详情的导航标题,以及展示模式。padding 设置视图间的间隔。
小结
以上虽然解决了 UI 构建的问题,但实际应用中,数据都是动态变化的,所以我们需要与后端服务器进行交互。下一篇文章小彭将继续为大家介绍 SwiftUI 的 ObservableObject、@ObservedObject、@Published 等特性。以及如何从服务器获取数据,实现新闻咨询的动态更新。
以上虽然解决了 UI 构建的问题,但实际应用中,数据都是动态变化的,所以我们需要与后端服务器进行交互。下一篇文章小彭将继续为大家介绍 SwiftUI 的 ObservableObject、@ObservedObject、@Published 等特性。以及如何从服务器获取数据,实现新闻咨询的动态更新。