SwiftUI - Navigation & List & Present

在這一節(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。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末产捞,一起剝皮案震驚了整個(gè)濱河市醇锚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌坯临,老刑警劉巖焊唬,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異看靠,居然都是意外死亡赶促,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門挟炬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鸥滨,“玉大人,你說我怎么就攤上這事谤祖⌒鲎遥” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵粥喜,是天一觀的道長(zhǎng)凸主。 經(jīng)常有香客問我,道長(zhǎng)额湘,這世上最難降的妖魔是什么卿吐? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮锋华,結(jié)果婚禮上嗡官,老公的妹妹穿的比我還像新娘。我一直安慰自己毯焕,他們只是感情好衍腥,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著纳猫,像睡著了一般紧阔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上续担,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天擅耽,我揣著相機(jī)與錄音,去河邊找鬼物遇。 笑死乖仇,一個(gè)胖子當(dāng)著我的面吹牛憾儒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播乃沙,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼起趾,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了警儒?” 一聲冷哼從身側(cè)響起训裆,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蜀铲,沒想到半個(gè)月后边琉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡记劝,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年变姨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厌丑。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡定欧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出怒竿,到底是詐尸還是另有隱情砍鸠,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布耕驰,位于F島的核電站爷辱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏耍属。R本人自食惡果不足惜托嚣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一巩检、第九天 我趴在偏房一處隱蔽的房頂上張望厚骗。 院中可真熱鬧,春花似錦兢哭、人聲如沸领舰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽冲秽。三九已至,卻和暖如春矩父,著一層夾襖步出監(jiān)牢的瞬間锉桑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工窍株, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留民轴,地道東北人攻柠。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像后裸,于是被迫代替她去往敵國(guó)和親瑰钮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容