在该项目中,我们将创建SnowSeeker:一款可让用户浏览世界各地滑雪胜地的应用程序,以帮助他们找到适合下一个假期的滑雪胜地。
这将是第一个我们专门旨在通过并排显示两个视图来使某些功能在iPad上发挥出色的应用程序,但您还将深入研究解决有问题的布局,学习显示工作表和警报的新方法,以及更多。
建立项目的主要清单
在此应用中,我们将同时显示两个视图,就像 Apple 的 Mail 和 Notes 应用一样。在 SwiftUI 中,这是通过将两个视图放入NavigationView中,然后在主视图中使用NavigationLink来控制在辅助视图中可见的内容来完成的。
因此,我们将通过为应用程序构建主视图来开始我们的项目,该视图将显示所有滑雪胜地的列表,它们来自哪个国家/地区以及拥有多少个滑雪道——您可以从多少个滑雪道滑下,有时称为“小径”或仅称为“斜坡”。
我已经在本书的GitHub存储库中为该项目提供了一些资源,因此,如果您尚未下载它们,请立即下载(下载地址见开篇Hacking with iOS: SwiftUI Edition文末)。您应该将 resorts.json 拖到项目导航器中,然后将所有图片复制到资源目录中。您可能会注意到,我为这些国家/地区添加了 2x 和 3x 图像,但为度假胜地仅添加了 2x 图片。这是故意的:这些标志将同时用于视网膜和Super Retina设备,但是度假村图片旨在填充iPad Pro的所有空间——即使在2倍分辨率下,它们也足以容纳Super Retina iPhone 。
为了快速启动并运行我们的列表,我们需要定义一个简单的Resort结构,该结构可以从JSON加载。这意味着它需要符合Codable,但是为了使其更易于在SwiftUI中使用,我们还将使其符合Identifiable。实际数据本身主要是字符串和整数,但是还有一个称为设施的字符串数组,它描述了度假村中还有什么——我应该补充一点,该数据主要是虚构的,所以不要尝试在真实环境中使用它!
创建一个名为 Resort.swift 的新Swift文件,然后为其提供以下代码:
struct Resort: Codable, Identifiable {
let id: String
let name: String
let country: String
let description: String
let imageCredit: String
let price: Int
let size: Int
let snowDepth: Int
let elevation: Int
let runs: Int
let facilities: [String]
}
static let allResorts: [Resort] = Bundle.main.decode("resorts.json")
static let example = allResorts[0]
decode()扩展方法需要知道其要解码的数据类型:static let example = (Bundle.main.decode("resorts.json") as [Resort])[0]
static let时,Swift会自动使它们变得懒惰——除非使用它们,否则它们不会被创建。这意味着当我们尝试阅读Resort.example时,Swift将被迫首先创建Resort.allResorts,然后将该数组中的第一项发送回给Resort.example。这意味着我们始终可以确保这两个属性将以正确的顺序运行——由于还没有调用allResorts,因此不会丢失示例。extension Bundle {
func decode(_ file: String) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}guard let data = https://www.it610.com/article/try? Data(contentsOf: url) else {
fatalError("Failed to load \(file) from bundle.")
}let decoder = JSONDecoder()guard let loaded = try? decoder.decode(T.self, from: data) else {
fatalError("Failed to decode \(file) from bundle.")
}return loaded
}
}
ContentView 添加一个属性,该属性将我们的所有度假村加载到单个数组中:let resorts: [Resort] = Bundle.main.decode("resorts.json")
NavigationView,以显示我们的所有度假胜地。在每一行中,我们将显示:resizable(),scaledToFit()和自定义框架来解决此问题。为了使它在屏幕上看起来更好一点,我们将使用自定义剪辑形状和描边叠加层。body属性:NavigationView {
List(resorts) { resort in
NavigationLink(destination: Text(resort.name)) {
Image(resort.country)
.resizable()
.scaledToFill()
.frame(width: 40, height: 25)
.clipShape(
RoundedRectangle(cornerRadius: 5)
)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.black, lineWidth: 1)
)VStack(alignment: .leading) {
Text(resort.name)
.font(.headline)
Text("\(resort.runs) runs")
.foregroundColor(.secondary)
}
}
}
.navigationBarTitle("Resorts")
}
NavigationView时,默认情况下,SwiftUI希望我们提供可以并排显示的主视图和辅助详细视图,主视图显示在左侧,辅助视图显示在右侧。以前,我们通过将StackNavigationViewStyle()用作NavigationView的导航样式来解决此问题,它告诉SwiftUI我们只想显示一个视图,但是在这里我们实际上想要的是两个视图的行为,因此我们将不使用它。ContentView。如果您点击其中的行,您将看到由于我们的NavigationLink而导致ContentView后面的文本发生了变化;如果您点击了后面的文本,则可以关闭ContentView的视图。WelcomeView的新SwiftUI视图,然后为其提供以下代码:struct WelcomeView: View {
var body: some View {
VStack {
Text("Welcome to SnowSeeker!")
.font(.largeTitle)Text("Please select a resort from the left-hand menu;
swipe from the left edge to show it.")
.foregroundColor(.secondary)
}
}
}
ContentView中,以便可以并排使用UI的两个部分,我们要做的就是向NavigationView中添加第二个视图,如下所示:NavigationView {
List(resorts) { resort in
// all the previous list code
}
.navigationBarTitle("Resorts")WelcomeView()
}
ContentView。ContentView和横向的WelcomeView。ContentView和横向的WelcomeView。extension View {
func phoneOnlyStackNavigationView() -> some View {
if UIDevice.current.userInterfaceIdiom == .phone {
return AnyView(self.navigationViewStyle(StackNavigationViewStyle()))
} else {
return AnyView(self)
}
}
}
UIDevice类来检测我们当前是在手机还是平板电脑上运行,如果是手机,则可以启用更简单的StackNavigationViewStyle方法。我们这里需要使用类型擦除,因为返回的两种视图类型不同。.phoneOnlyStackNavigationView()修饰符添加到NavigationView中,以便iPad保留其默认行为,而iPhone始终使用堆栈导航。NavigationLink将用户引导到一些示例文本,这对于原型设计很好,但是对于我们的实际项目来说显然不够好。我们将用一个新的ResortView来替换它,该视图显示度假胜地的图片、一些描述文本和设施列表。restorview布局将非常简单——只不过是一个滚动视图、一个VStack、一个Image和一些Text。唯一有趣的部分是,我们将使用resort.facilities.joined(separator: ", ")以获取单个字符串。ResortView视图替换为:struct ResortView: View {
let resort: Resortvar body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
Image(decorative: resort.id)
.resizable()
.scaledToFit()Group {
Text(resort.description)
.padding(.vertical)Text("Facilities")
.font(.headline)Text(resort.facilities.joined(separator: ", "))
.padding(.vertical)
}
.padding(.horizontal)
}
}
.navigationBarTitle(Text("\(resort.name), \(resort.country)"), displayMode: .inline)
}
}
ResortView_Previews,以便传入Xcode预览窗口的示例旅游地:struct ResortView_Previews: PreviewProvider {
static var previews: some View {
ResortView(resort: Resort.example)
}
}
ContentView中的导航链接,以指向实际视图,如下所示:NavigationLink(destination: ResortView(resort: resort)) {
HStack中,但是这限制了我们将来可以做什么。因此,我们将把它们分为两个视图:一个用于度假村信息(价格和大小),另一个用于滑雪信息(海拔和积雪深度)。SkiDetailsView的新SwiftUI视图,并给出以下代码:struct SkiDetailsView: View {
let resort: Resortvar body: some View {
VStack {
Text("Elevation: \(resort.elevation)m")
Text("Snow: \(resort.snowDepth)cm")
}
}
}struct SkiDetailsView_Previews: PreviewProvider {
static var previews: some View {
SkiDetailsView(resort: Resort.example)
}
}
size和price。ResortDetailsView的新SwiftUI视图,并为其指定以下属性:let resort: Resort
RestorView一样,您需要更新preview结构体以使用一些示例数据:struct ResortDetailsView_Previews: PreviewProvider {
static var previews: some View {
ResortDetailsView(resort: Resort.example)
}
}
ResortDetailsView:var size: String {
["Small", "Average", "Large"][resort.size - 1]
}
switch代码块更安全、更清晰:var size: String {
switch resort.size {
case 1:
return "Small"
case 2:
return "Average"
default:
return "Large"
}
}
price属性,我们可以利用与在project17中创建示例卡片时使用的String(repeating:count:)通过将子字符串重复一定次数来创建新字符串。ResortDetailsView:var price: String {
String(repeating: "$", count: resort.price)
}
body属性中剩下的内容很简单,因为我们只使用我们编写的两个计算属性:var body: some View {
VStack {
Text("Size: \(size)")
Text("Price: \(price)")
}
}
ResortView中,两边都有间隔符,以确保它们居中——将其放入ResortView中的组中,就在度假胜地描述之前:HStack {
Spacer()
ResortDetailsView(resort: resort)
SkiDetailsView(resort: resort)
Spacer()
}
.font(.headline)
.foregroundColor(.secondary)
.padding(.top)
joined(separator:)可以将字符串数组转换为单个字符串,但我们不是来编写一般可用代码的——我们是来编写出色的代码的。ListFormatter,它只有一项工作:将字符串数组转换为字符串。不同的是,我们没有像现在那样返回“A,B,C”,而是返回“A,B 和 C”——阅读起来更自然。ListFormatter,请将当前设施文本视图替换为:Text(ListFormatter.localizedString(byJoining: resort.facilities))
.padding(.vertical)
译自
Building a primary list of items
Making NavigationView work in landscape
Creating a secondary view for NavigationView
上一篇:日更第21天
下一篇:桁架搭建有什么价值()