美文网首页IOS开发
SwiftUI - Navigation & List & Pr

SwiftUI - Navigation & List & Pr

作者: 西西的一天 | 来源:发表于2019-12-28 20:42 被阅读0次

在这一节中,将介绍如何使用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。

相关文章

网友评论

    本文标题:SwiftUI - Navigation & List & Pr

    本文链接:https://www.haomeiwen.com/subject/nqvsoctx.html