在這一節(jié)中杯矩,將介紹如何使用SwiftUI來實(shí)現(xiàn)UIKit中的UITabBarController,UINavigationController,以及UITableView渐排。
UIKit的中導(dǎo)航是基于UIViewController容器,也就是UITabBarController灸蟆,UINavigationController飞盆,由容器來管理多個(gè)UIViewController實(shí)例。這些UIViewController實(shí)例間的關(guān)系分為兩種,其一是平級(jí)關(guān)系吓歇,也就是UITabBarController中的多個(gè)Tab孽水;其二是父子關(guān)系,比如基于NavigationController的master城看,detail視圖女气。
而SwiftUI中,所有呈現(xiàn)在界面上的皆是View實(shí)例测柠,包括復(fù)雜導(dǎo)航的View容器也是一種View實(shí)例炼鞠。
UITabBarController -> TabView
先來看一下取代UITabBarController的TabView『湫玻回憶一下UIKit時(shí)的Tab谒主,通常UIWindow的rootViewController是一個(gè)UITabBarController,作為根導(dǎo)航容器赃阀,通過設(shè)置UITabBarController的viewControllers這個(gè)屬性霎肯,來設(shè)置多個(gè)平級(jí)UIViewController。在SwiftUI中流程大致是相同的:
let contentView = ContentView()
window.rootViewController = UIHostingController(rootView: contentView)
根視圖是這個(gè)ContentView榛斯,這個(gè)ContentView的實(shí)現(xiàn)中:
struct ContentView: View {
var body: some View {
TabView {
FlightBoard()
.tabItem ({
Image(systemName: "icloud.and.arrow.down").resizable()
Text("Arrivals")
})
FlightBoard()
.tabItem ({
Image(systemName: "icloud.and.arrow.up").resizable()
Text("Departures")
})
}
}
}
TabView便起到了導(dǎo)航的作用观游,TabView中可以保護(hù)多個(gè)View元素,每個(gè)View便是一個(gè)Tab驮俗。在上述代碼中FlightBoard()
只是一個(gè)普通的View:
struct FlightBoard: View {
var body: some View {
Text("Hello World!")
}
}
通過設(shè)置View的tabItem來配置Tab的圖片以及文字懂缕。相比UIKit中Tab實(shí)現(xiàn)方式,TabView顯得更加直接王凑,簡(jiǎn)單明了搪柑。
UINavigationController -> NavigationView
了解了TabView的基本用法后,NavigationView就比較容易上手了索烹,代碼如下:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: FlightBoard(boardName: "Arrivals")) {
HStack {
Image(systemName: "icloud.and.arrow.down").resizable().frame(width: 30, height: 30)
Text("Arrivals")
}
}
NavigationLink(destination: FlightBoard(boardName: "Departures")) {
HStack {
Image(systemName: "icloud.and.arrow.up").resizable().frame(width: 30, height: 30)
Text("Departures")
}
}
}.navigationBarTitle(Text("Mountain Airport"))
}
}
}
稍微修改一下FlightBoard:
struct FlightBoard: View {
let boardName: String
var body: some View {
VStack {
Text(boardName)
}.navigationBarTitle(Text(boardName))
}
}
不同于TabView拌屏,NavigationView只允許保護(hù)一個(gè)View元素,通常是一個(gè)VStack類的容器术荤,而Navigation的Title也是通過給該容器添加一個(gè)修飾符navigationBarTitle來實(shí)現(xiàn)倚喂。
UITableView -> List
在演示List之前,回想一下在UIKit中另一個(gè)可以滾動(dòng)的View:UIScrollView瓣戚,它在SwiftUI中為ScrollView端圈,為了展示可以滾動(dòng)的效果,來創(chuàng)造一下數(shù)據(jù)子库,修改FlightBoard:
struct FlightBoard: View {
let boardName: String
let flightData: [FlightInformation]
var body: some View {
VStack {
Text(boardName).font(.title)
ScrollView(showsIndicators: false) {
ForEach(flightData) { fl in
VStack {
Text("\(fl.airline) \(fl.number)")
Text("\(fl.flightStatus) at \(fl.currentTimeString)")
Text("At gate \(fl.gate)")
}
}
}
}.navigationBarTitle(Text(boardName))
}
}
上述代碼中let flightData: [FlightInformation]
為上層視圖傳入的一個(gè)model數(shù)組舱权,ScrollView的用法也簡(jiǎn)單明了,和一般的Stack相同仑嗅,放入一組View數(shù)組即可宴倍,此處用了ForEach张症,看一下它的定義:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable {
/// The collection of underlying identified data.
public var data: Data
/// A function that can be used to generate content on demand given
/// underlying data.
public var content: (Data.Element) -> Content
}
從定義可以看出,它的構(gòu)造函數(shù)需要一個(gè)Data
鸵贬,以及一個(gè)Block俗他,Block很好理解,就是在遍歷Data中的每個(gè)元素阔逼。而這個(gè)Data
是一個(gè)泛型兆衅,它需要實(shí)現(xiàn)RandomAccessCollection協(xié)議,這個(gè)協(xié)議要求可以通過角標(biāo)的方式訪問集合中的元素嗜浮,Swift的Array類型也實(shí)現(xiàn)了該協(xié)議羡亩,可以把Data理解為一個(gè)數(shù)組。
extension Array : RandomAccessCollection, MutableCollection {...}
同時(shí)ForEach也要求每個(gè)元素都有一個(gè)ID屬性危融,而這個(gè)ID需要是在當(dāng)前列表中是唯一的畏铆。實(shí)現(xiàn)方式也很簡(jiǎn)單,只需要讓FlightInformation吉殃,實(shí)現(xiàn)一個(gè)Identifiable協(xié)議:
extension FlightInformation : Identifiable { }
/// A class of types whose instances hold the value of an entity with stable identity.
@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *)
public protocol Identifiable {
/// A type representing the stable identity of the entity associated with `self`.
associatedtype ID : Hashable
/// The stable identity of the entity associated with `self`.
var id: Self.ID { get }
}
看完了ScrollView辞居,我們可以進(jìn)入主題List,直接將上方代碼的ScrollView換為L(zhǎng)ist即可寨腔,對(duì)比UITableView,我們需要一個(gè)Cell率寡,而Cell在SwiftUI就是一個(gè)普通的View:
struct FlightBoard: View {
let boardName: String
let flightData: [FlightInformation]
var body: some View {
VStack {
List(flightData) { fl in
FlightRow(flight: fl)
}
}.navigationBarTitle(Text(boardName), displayMode: NavigationBarItem.TitleDisplayMode.large)
}
}
struct FlightRow: View {
let flight: FlightInformation
var body: some View {
HStack {
Text("\(self.flight.airline) \(self.flight.number)")
.frame(width: 120, alignment: .leading)
Text(self.flight.otherAirport).frame(alignment: .leading)
Spacer()
Text(self.flight.flightStatus).frame(alignment: .trailing)
}
}
}
那么如何給這個(gè)列表中的每一項(xiàng)添加點(diǎn)擊效果呢迫卢?點(diǎn)擊一個(gè)cell跳轉(zhuǎn)到一個(gè)detail頁面,先添加一個(gè)Detail頁面:
struct FlightBoardInformation: View {
let flight: FlightInformation
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("\(flight.airline) Flight \(flight.number)")
.font(.largeTitle)
Spacer()
}
Text("\(flight.direction == .arrival ? "From: " : "To: ") \(flight.otherAirport)")
Text(flight.flightStatus)
.foregroundColor(Color(flight.timelineColor))
Spacer()
}.font(.headline).padding(10)
}
}
跳轉(zhuǎn)邏輯也很簡(jiǎn)單冶共,只需要將cell的視圖包裹在NavigationLink即可:
List(flightData) { fl in
NavigationLink(destination: FlightBoardInformation(flight: fl)) {
FlightRow(flight: fl)
}
}
Present
在UIKit中乾蛤,可以調(diào)用UIViewController的present方法來開啟一個(gè)modal形式的頁面,SwiftUI中操作的均為View捅僵,實(shí)現(xiàn)方式會(huì)有所區(qū)別家卖,通過一個(gè)布爾類型@State來控制顯示和消失。改一下上面的List:
List(flightData) { fl in
FlightRow(flight: fl)
}
同時(shí)也更改一下cell的實(shí)現(xiàn)庙楚,presented頁面是通過這個(gè)cell也就是這個(gè)button點(diǎn)擊觸發(fā)的上荡,而這個(gè)sheet定義在View的一個(gè)extension中:
struct FlightRow: View {
let flight: FlightInformation
@State private var isPresented = false
var body: some View {
Button(action: {
self.isPresented.toggle()
}) {
HStack {
Text("\(self.flight.airline) \(self.flight.number)")
.frame(width: 120, alignment: .leading)
Text(self.flight.otherAirport).frame(alignment: .leading)
Spacer()
Text(self.flight.flightStatus).frame(alignment: .trailing)
}.sheet(isPresented: $isPresented, onDismiss: {
print("Modal dismissed. State now: \(self.isPresented)")
}) {
FlightBoardInformation(showModel: self.$isPresented, flight: self.flight)
}
}
}
}
在上述實(shí)現(xiàn)中,也把這個(gè)isPresented的引用傳遞了進(jìn)去馒闷,傳遞進(jìn)入的目的酪捡,是想要在彈出的頁面中控制presented view的收起:
struct FlightBoardInformation: View {
@Binding var showModel: Bool
let flight: FlightInformation
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("\(flight.airline) Flight \(flight.number)")
.font(.largeTitle)
Spacer()
Button("Done") {
self.showModel = false
}
}
Text("\(flight.direction == .arrival ? "From: " : "To: ") \(flight.otherAirport)")
Text(flight.flightStatus)
.foregroundColor(Color(flight.timelineColor))
Spacer()
}.font(.headline).padding(10)
}
}
這種實(shí)現(xiàn)方式和React Native非常相似,頁面的展示結(jié)果纳账,和一個(gè)state變量進(jìn)行了綁定逛薇,想要更新頁面只需要修改一個(gè)變量即可。
Alert
看完了present疏虫,alert和它十分相似永罚,alert的展示同樣也是通過一個(gè)state來控制的啤呼,我們改一下FlightBoardInformation的實(shí)現(xiàn):
struct FlightBoardInformation: View {
@Binding var showModel: Bool
let flight: FlightInformation
@State private var rebootAlert = false
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("\(flight.airline) Flight \(flight.number)")
.font(.largeTitle)
Spacer()
Button("Done") {
self.showModel = false
}
}
Text("\(flight.direction == .arrival ? "From: " : "To: ") \(flight.otherAirport)")
Text(flight.flightStatus)
.foregroundColor(Color(flight.timelineColor))
if flight.status == .cancelled {
Button("Reboot Flight") {
self.rebootAlert = true
}.alert(isPresented: $rebootAlert) { () -> Alert in
Alert(
title: Text("Contact Your Airline"),
message: Text("We cannot rebook this flight. Please contact the airline to reschedule this flight.")
)
}
}
Spacer()
}.font(.headline).padding(10)
}
}
上述代碼中的button點(diǎn)擊事件就是將rebootAlert設(shè)置為true。而彈出的alert默認(rèn)會(huì)有一個(gè)button呢袱,點(diǎn)擊它會(huì)將rebootAlert設(shè)置為false官扣,從而關(guān)閉alert。