SwiftUI - Navigation & List & Present

在这一节中,将介绍如何使用SwiftUI来实现UIKit中的UITabBarController,UINavigationController,以及UITableView。

UIKit的中导航是基于UIViewController容器,也就是UITabBarController,UINavigationController,由容器来管理多个UIViewController实例。这些UIViewController实例间的关系分为两种,其一是平级关系,也就是UITabBarController中的多个Tab;其二是父子关系,比如基于NavigationController的master,detail视图。

而SwiftUI中,所有呈现在界面上的皆是View实例,包括复杂导航的View容器也是一种View实例。

UITabBarController -> TabView

先来看一下取代UITabBarController的TabView。回忆一下UIKit时的Tab,通常UIWindow的rootViewController是一个UITabBarController,作为根导航容器,通过设置UITabBarController的viewControllers这个属性,来设置多个平级UIViewController。在SwiftUI中流程大致是相同的:

let contentView = ContentView()
window.rootViewController = UIHostingController(rootView: contentView)

根视图是这个ContentView,这个ContentView的实现中:

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便起到了导航的作用,TabView中可以保护多个View元素,每个View便是一个Tab。在上述代码中FlightBoard()只是一个普通的View:

struct FlightBoard: View {
    var body: some View {
        Text("Hello World!")
    }
}

通过设置View的tabItem来配置Tab的图片以及文字。相比UIKit中Tab实现方式,TabView显得更加直接,简单明了。

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只允许保护一个View元素,通常是一个VStack类的容器,而Navigation的Title也是通过给该容器添加一个修饰符navigationBarTitle来实现。

UITableView -> List

在演示List之前,回想一下在UIKit中另一个可以滚动的View:UIScrollView,它在SwiftUI中为ScrollView,为了展示可以滚动的效果,来创造一下数据,修改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]为上层视图传入的一个model数组,ScrollView的用法也简单明了,和一般的Stack相同,放入一组View数组即可,此处用了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
}

从定义可以看出,它的构造函数需要一个Data,以及一个Block,Block很好理解,就是在遍历Data中的每个元素。而这个Data是一个泛型,它需要实现RandomAccessCollection协议,这个协议要求可以通过角标的方式访问集合中的元素,Swift的Array类型也实现了该协议,可以把Data理解为一个数组。

extension Array : RandomAccessCollection, MutableCollection {...}

同时ForEach也要求每个元素都有一个ID属性,而这个ID需要是在当前列表中是唯一的。实现方式也很简单,只需要让FlightInformation,实现一个Identifiable协议:

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,我们可以进入主题List,直接将上方代码的ScrollView换为List即可,对比UITableView,我们需要一个Cell,而Cell在SwiftUI就是一个普通的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)
        }
    }
}

那么如何给这个列表中的每一项添加点击效果呢?点击一个cell跳转到一个detail页面,先添加一个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)
    }
}

跳转逻辑也很简单,只需要将cell的视图包裹在NavigationLink即可:

List(flightData) { fl in
  NavigationLink(destination: FlightBoardInformation(flight: fl)) {
    FlightRow(flight: fl)
  }
}

Present

在UIKit中,可以调用UIViewController的present方法来开启一个modal形式的页面,SwiftUI中操作的均为View,实现方式会有所区别,通过一个布尔类型@State来控制显示和消失。改一下上面的List:

List(flightData) { fl in
  FlightRow(flight: fl)
}

同时也更改一下cell的实现,presented页面是通过这个cell也就是这个button点击触发的,而这个sheet定义在View的一个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)
            }
        }
    }
}

在上述实现中,也把这个isPresented的引用传递了进去,传递进入的目的,是想要在弹出的页面中控制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)
    }
}

这种实现方式和React Native非常相似,页面的展示结果,和一个state变量进行了绑定,想要更新页面只需要修改一个变量即可。

Alert

看完了present,alert和它十分相似,alert的展示同样也是通过一个state来控制的,我们改一下FlightBoardInformation的实现:

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点击事件就是将rebootAlert设置为true。而弹出的alert默认会有一个button,点击它会将rebootAlert设置为false,从而关闭alert。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容