SwiftUI简介
SwiftUI是一种更为现代的编码方式,他可以为苹果任意平台声明用户界面。以更为快捷迅速的方式创建漂亮的动态应用程序!
只需描述你的布局
为视图的任何状态声明内容和布局。SwiftUI知道该状态何时更改,并更新视图,以匹配其呈现的状态。
1 2 3 4 5 6 7 8 9 10 11 12 List (landmarks) { landmark in HStack { Image (landmark.thumbnail) Text (landmark.name) Spacer () if landmark.isFavorite { Image (systemName: "star.fill" ) .foregroundColor(.yellow) } } }
构建可重用组件
将小的、单一职责的视图组合成更大、更复杂的界面。在为苹果任意平台设计的应用程序之间共享您的自定义视图。
1 2 3 4 5 6 7 8 9 10 struct FeatureCard : View { var landmark: Landmark var body: some View { landmark.featureImage .resizable() .aspectRatio(3 / 2 , contentMode: .fit) .overlay(TextOverlay (landmark)) } }
简化动画
创建平滑动画就像添加一个方法调用一样简单。SwiftUI会在需要时自动计算并设置过渡动画。
1 2 3 4 5 6 7 8 VStack { Badge () .frame(width: 300 , height: 300 ) .animation(.easeInOut()) Text (name) .font(.title) .animation(.easeInOut()) }
在Xcode中实时预览
在不运行应用程序的情况下设计、生成和测试应用程序的界面。使用交互式预览测试控件和布局。
应用程序设计和布局
组合复杂界面
地标的首页显示一个滚动的分类列表,在每个分类中水平滚动地标。在构建此主导航时,您将探索组合视图如何适应不同的设备大小和方向。
学习时间:20分钟
下载地址:ComposingComplexInterfaces.zip
第一节 添加首页
既然你已经拥有了地标应用程序所需的所有视图,现在是时候给它们一个首页了——一个统一视图页面。主视图不仅包含所有其他视图,还提供导航和显示地标的方法。
步骤1
在名为Home.swift
的新文件中创建名为CategoryHome
的自定义视图。
Home.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 import SwiftUIstruct CategoryHome : View { var body: some View { Text ("Landmarks Content" ) } } struct CategoryHome_Previews : PreviewProvider { static var previews: some View { CategoryHome () } }
步骤2
修改scene delegate
,使其显示新的CategoryHome
视图而不是地标列表。
主视图是地标应用程序的根,因此它需要一种方式来呈现所有其他视图。
SceneDelegate.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import SwiftUIimport UIKitclass SceneDelegate : UIResponder , UIWindowSceneDelegate { var window: UIWindow ? func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene .ConnectionOptions ) { if let windowScene = scene as? UIWindowScene { let window = UIWindow (windowScene: windowScene) window.rootViewController = UIHostingController ( rootView: CategoryHome () .environmentObject(UserData ()) ) self .window = window window.makeKeyAndVisible() } } }
步骤3
添加NavigationView
以在地标中承载不同的视图。
您可以使用NavigationView
以及NavigationLink
实例和相关修饰符在应用程序中构建导航层级结构。
Home.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import SwiftUIstruct CategoryHome : View { var body: some View { NavigationView { Text ("Landmarks Content" ) } } } struct CategoryHome_Previews : PreviewProvider { static var previews: some View { CategoryHome () } }
步骤4
将导航栏的标题设置为“Featured”。
Home.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import SwiftUIstruct CategoryHome : View { var body: some View { NavigationView { Text ("Landmarks Content" ) .navigationBarTitle(Text ("Featured" )) } } } struct CategoryHome_Previews : PreviewProvider { static var previews: some View { CategoryHome () } }
第二节 创建分类列表
Landmarks应用程序将所有分类显示在垂直列中的单独行中,以便于浏览。通过组合垂直和水平堆栈,并向列表中添加滚动条,就可以完成此操作。
步骤1
使用Dictionary
结构的init(grouping:by:)
方法将地标分组到类别中,输入地标的category属性。
初始化项目文件给每个示例地标预设了类别。
Home.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import SwiftUIstruct CategoryHome : View { var categories: [String : [Landmark ]] { Dictionary ( grouping: landmarkData, by: { $0 .category.rawValue } ) } var body: some View { NavigationView { Text ("Landmarks Content" ) .navigationBarTitle(Text ("Featured" )) } } } struct CategoryHome_Previews : PreviewProvider { static var previews: some View { CategoryHome () } }
步骤2
使用列表在地标中显示分类。
Landmark.Category
会匹配列表中每一项name
,这些项在其他类别中必须是唯一的,因为它是枚举。
Home.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import SwiftUIstruct CategoryHome : View { var categories: [String : [Landmark ]] { Dictionary ( grouping: landmarkData, by: { $0 .category.rawValue } ) } var body: some View { NavigationView { List { ForEach (categories.keys.sorted(), id: \.self ) { key in Text (key) } } .navigationBarTitle(Text ("Featured" )) } } } struct CategoryHome_Previews : PreviewProvider { static var previews: some View { CategoryHome () } }
第三节 添加地标行
地标在水平滚动的行中显示每个类别。添加一个新的视图类型来表示行,然后在新视图中显示该类别的所有地标。
步骤1
定义用于保存行内容的新自定义视图。
这个视图需要存储地标的类别信息,以及地标本身。
CategoryRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import SwiftUIstruct CategoryRow : View { var categoryName: String var items: [Landmark ] var body: some View { Text (self .categoryName) .font(.headline) } } struct CategoryRow_Previews : PreviewProvider { static var previews: some View { CategoryRow ( categoryName: landmarkData[0 ].category.rawValue, items: Array (landmarkData.prefix(3 )) ) } }
步骤2
更新CategoryHome
的主体以将类别信息传递给新的行类型。
Home.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import SwiftUIstruct CategoryHome : View { var categories: [String : [Landmark ]] { Dictionary ( grouping: landmarkData, by: { $0 .category.rawValue } ) } var body: some View { NavigationView { List { ForEach (categories.keys.sorted(), id: \.self ) { key in CategoryRow (categoryName: key, items: self .categories[key]! ) } } .navigationBarTitle(Text ("Featured" )) } } } struct CategoryHome_Previews : PreviewProvider { static var previews: some View { CategoryHome () } }
步骤3
在HStack
中显示该类别的地标。
CategoryRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import SwiftUIstruct CategoryRow : View { var categoryName: String var items: [Landmark ] var body: some View { HStack (alignment: .top, spacing: 0 ) { ForEach (self .items) { landmark in Text (landmark.name) } } } } struct CategoryRow_Previews : PreviewProvider { static var previews: some View { CategoryRow ( categoryName: landmarkData[0 ].category.rawValue, items: Array (landmarkData.prefix(3 )) ) } }
步骤4
通过指定一个frame(width:height:)
并在scrollView
中包装stack
,为行提供空间。
使用长数据样本更新视图预览,以便确保可以滚动。
CategoryRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import SwiftUIstruct CategoryRow : View { var categoryName: String var items: [Landmark ] var body: some View { VStack (alignment: .leading) { Text (self .categoryName) .font(.headline) .padding(.leading, 15 ) .padding(.top, 5 ) ScrollView (.horizontal, showsIndicators: false ) { HStack (alignment: .top, spacing: 0 ) { ForEach (self .items) { landmark in Text (landmark.name) } } } .frame(height: 185 ) } } } struct CategoryRow_Previews : PreviewProvider { static var previews: some View { CategoryRow ( categoryName: landmarkData[0 ].category.rawValue, items: Array (landmarkData.prefix(4 )) ) } }
第四节 组成主页
地标应用程序的主页需要显示地标的简单信息,然后用户点击其中一个更多进入详情视图。
重新使用在Creating and Combining Views
中的视图来创建更简单的视图预览,以显示地标的分类和特征。
步骤1
在CategoryRow
旁边创建一个名为CategoryItem
的新自定义视图,并用新视图替换Text
视图。
CategoryRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import SwiftUIstruct CategoryRow : View { var categoryName: String var items: [Landmark ] var body: some View { VStack (alignment: .leading) { Text (self .categoryName) .font(.headline) .padding(.leading, 15 ) .padding(.top, 5 ) ScrollView (.horizontal, showsIndicators: false ) { HStack (alignment: .top, spacing: 0 ) { ForEach (self .items) { landmark in CategoryItem (landmark: landmark) } } } .frame(height: 185 ) } } } struct CategoryItem : View { var landmark: Landmark var body: some View { VStack (alignment: .leading) { landmark.image .resizable() .frame(width: 155 , height: 155 ) .cornerRadius(5 ) Text (landmark.name) .font(.caption) } .padding(.leading, 15 ) } } struct CategoryRow_Previews : PreviewProvider { static var previews: some View { CategoryRow ( categoryName: landmarkData[0 ].category.rawValue, items: Array (landmarkData.prefix(4 )) ) } }
步骤2
在Home.swift
中,添加一个名为FeaturedLandmarks
的简单视图,该视图仅显示标记为isFeatured
的地标。
在后面的教程中,您将把此视图转换为可交互的轮播视图。目前,它显示了一个裁剪和缩放后的预览图像。
Home.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import SwiftUIstruct CategoryHome : View { var categories: [String : [Landmark ]] { Dictionary ( grouping: landmarkData, by: { $0 .category.rawValue } ) } var featured: [Landmark ] { landmarkData.filter { $0 .isFeatured } } var body: some View { NavigationView { List { FeaturedLandmarks (landmarks: featured) .scaledToFill() .frame(height: 200 ) .clipped() ForEach (categories.keys.sorted(), id: \.self ) { key in CategoryRow (categoryName: key, items: self .categories[key]! ) } } .navigationBarTitle(Text ("Featured" )) } } } struct FeaturedLandmarks : View { var landmarks: [Landmark ] var body: some View { landmarks[0 ].image.resizable() } } struct CategoryHome_Previews : PreviewProvider { static var previews: some View { CategoryHome () } }
步骤3
在两种地标预览中将edge insets
设置为零,以便内容可以延伸到屏幕边缘。
Home.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import SwiftUIstruct CategoryHome : View { var categories: [String : [Landmark ]] { Dictionary ( grouping: landmarkData, by: { $0 .category.rawValue } ) } var featured: [Landmark ] { landmarkData.filter { $0 .isFeatured } } var body: some View { NavigationView { List { FeaturedLandmarks (landmarks: featured) .scaledToFill() .frame(height: 200 ) .clipped() .listRowInsets(EdgeInsets ()) ForEach (categories.keys.sorted(), id: \.self ) { key in CategoryRow (categoryName: key, items: self .categories[key]! ) } .listRowInsets(EdgeInsets ()) } .navigationBarTitle(Text ("Featured" )) } } } struct FeaturedLandmarks : View { var landmarks: [Landmark ] var body: some View { landmarks[0 ].image.resizable() } } struct CategoryHome_Previews : PreviewProvider { static var previews: some View { CategoryHome () } }
第五节 在分区之间添加导航
现在,在主页中可以看到所有不同分类的地标,用户需要一种方法来访问应用程序中的每个部分。使用navigation
和presentation Apis
可以从主页导航到地标详情页,收藏夹和用户主页。
步骤1
在CategoryRow.swift
中,用NavigationLink
包装现有的CategoryItem
。
类别项本身是按钮的label
,其目的地是显示地标详情视图。
CategoryRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import SwiftUIstruct CategoryRow : View { var categoryName: String var items: [Landmark ] var body: some View { VStack (alignment: .leading) { Text (self .categoryName) .font(.headline) .padding(.leading, 15 ) .padding(.top, 5 ) ScrollView (.horizontal, showsIndicators: false ) { HStack (alignment: .top, spacing: 0 ) { ForEach (self .items) { landmark in NavigationLink ( destination: LandmarkDetail ( landmark: landmark ) ) { CategoryItem (landmark: landmark) } } } } .frame(height: 185 ) } } } struct CategoryItem : View { var landmark: Landmark var body: some View { VStack (alignment: .leading) { landmark.image .resizable() .frame(width: 155 , height: 155 ) .cornerRadius(5 ) Text (landmark.name) .font(.caption) } .padding(.leading, 15 ) } } struct CategoryRow_Previews : PreviewProvider { static var previews: some View { CategoryRow ( categoryName: landmarkData[0 ].category.rawValue, items: Array (landmarkData.prefix(4 )) ) } }
注意
在Xcode11 beta 6
中,如果在列表中嵌套一个ScrollView
,并且该ScrollView
包含一个NavigationLink
,那么当用户点击NavigationLink
时,这些NavigationLink
不会导航到目标视图。
步骤2
通过应用renderingMode(:)
和foregroundColor(:)
修饰符更改导航外观。
作为NavigationLink
的label
传递的文本使用环境的强调色进行渲染,而图像可能被作为template images
进行渲染。您可以修改任一行为以最适合您的设计。
CategoryRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import SwiftUIstruct CategoryRow : View { var categoryName: String var items: [Landmark ] var body: some View { VStack (alignment: .leading) { Text (self .categoryName) .font(.headline) .padding(.leading, 15 ) .padding(.top, 5 ) ScrollView (.horizontal, showsIndicators: false ) { HStack (alignment: .top, spacing: 0 ) { ForEach (self .items) { landmark in NavigationLink ( destination: LandmarkDetail ( landmark: landmark ) ) { CategoryItem (landmark: landmark) } } } } .frame(height: 185 ) } } } struct CategoryItem : View { var landmark: Landmark var body: some View { VStack (alignment: .leading) { landmark.image .renderingMode(.original) .resizable() .frame(width: 155 , height: 155 ) .cornerRadius(5 ) Text (landmark.name) .foregroundColor(.primary) .font(.caption) } .padding(.leading, 15 ) } } struct CategoryRow_Previews : PreviewProvider { static var previews: some View { CategoryRow ( categoryName: landmarkData[0 ].category.rawValue, items: Array (landmarkData.prefix(4 )) ) } }
步骤3
在Home.swift
中,点击选项卡栏中的简介图标后,添加一个模态视图以显示用户简介页面。
当showProfile
状态变量设置为true
时,SwiftUI显示用户简介占位符,当用户关闭模态时,将showProfile
设置回false
。
Home.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import SwiftUIstruct CategoryHome : View { var categories: [String : [Landmark ]] { Dictionary ( grouping: landmarkData, by: { $0 .category.rawValue } ) } var featured: [Landmark ] { landmarkData.filter { $0 .isFeatured } } @State var showingProfile = false var body: some View { NavigationView { List { FeaturedLandmarks (landmarks: featured) .scaledToFill() .frame(height: 200 ) .clipped() .listRowInsets(EdgeInsets ()) ForEach (categories.keys.sorted(), id: \.self ) { key in CategoryRow (categoryName: key, items: self .categories[key]! ) } .listRowInsets(EdgeInsets ()) } .navigationBarTitle(Text ("Featured" )) .sheet(isPresented: $showingProfile ) { Text ("User Profile" ) } } } } struct FeaturedLandmarks : View { var landmarks: [Landmark ] var body: some View { landmarks[0 ].image.resizable() } } struct CategoryHome_Previews : PreviewProvider { static var previews: some View { CategoryHome () } }
步骤4
在导航栏中添加一个按钮,在点击时将showProfile
从false
切换为true
。
Home.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import SwiftUIstruct CategoryHome : View { var categories: [String : [Landmark ]] { .init ( grouping: landmarkData, by: { $0 .category.rawValue } ) } var featured: [Landmark ] { landmarkData.filter { $0 .isFeatured } } @State var showingProfile = false var profileButton: some View { Button (action: { self .showingProfile.toggle() }) { Image (systemName: "person.crop.circle" ) .imageScale(.large) .accessibility(label: Text ("User Profile" )) .padding() } } var body: some View { NavigationView { List { FeaturedLandmarks (landmarks: featured) .scaledToFill() .frame(height: 200 ) .clipped() .listRowInsets(EdgeInsets ()) ForEach (categories.keys.sorted(), id: \.self ) { key in CategoryRow (categoryName: key, items: self .categories[key]! ) } .listRowInsets(EdgeInsets ()) } .navigationBarTitle(Text ("Featured" )) .navigationBarItems(trailing: profileButton) .sheet(isPresented: $showingProfile ) { Text ("User Profile" ) } } } } struct FeaturedLandmarks : View { var landmarks: [Landmark ] var body: some View { landmarks[0 ].image(forSize: 250 ).resizable() } } struct CategoryHome_Previews : PreviewProvider { static var previews: some View { CategoryHome () } }
步骤5
通过添加导航链接完成主页,该链接指向可过滤所有地标的列表。
Home.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import SwiftUIstruct CategoryHome : View { var categories: [String : [Landmark ]] { Dictionary ( grouping: landmarkData, by: { $0 .category.rawValue } ) } var featured: [Landmark ] { landmarkData.filter { $0 .isFeatured } } @State var showingProfile = false var profileButton: some View { Button (action: { self .showingProfile.toggle() }) { Image (systemName: "person.crop.circle" ) .imageScale(.large) .accessibility(label: Text ("User Profile" )) .padding() } } var body: some View { NavigationView { List { FeaturedLandmarks (landmarks: featured) .scaledToFill() .frame(height: 200 ) .clipped() .listRowInsets(EdgeInsets ()) ForEach (categories.keys.sorted(), id: \.self ) { key in CategoryRow (categoryName: key, items: self .categories[key]! ) } .listRowInsets(EdgeInsets ()) NavigationLink (destination: LandmarkList ()) { Text ("See All" ) } } .navigationBarTitle(Text ("Featured" )) .navigationBarItems(trailing: profileButton) .sheet(isPresented: $showingProfile ) { Text ("User Profile" ) } } } } struct FeaturedLandmarks : View { var landmarks: [Landmark ] var body: some View { landmarks[0 ].image.resizable() } } struct CategoryHome_Previews : PreviewProvider { static var previews: some View { CategoryHome () } }
步骤6
在LandmarkList.swift
中,删除包装地标列表的NavigationView
,并将其添加到预览中。
在应用程序的环境中,LandmarkList
将始终显示在Home.swift
中声明的导航视图中。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import SwiftUIstruct LandmarkList : View { @EnvironmentObject var userData: UserData var body: some View { List { Toggle (isOn: $userData .showFavoritesOnly) { Text ("Favorites only" ) } ForEach (userData.landmarks) { landmark in if ! self .userData.showFavoritesOnly || landmark.isFavorite { NavigationLink (destination: LandmarkDetail (landmark: landmark)) { LandmarkRow (landmark: landmark) } } } } .navigationBarTitle(Text ("Landmarks" )) } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { NavigationView { LandmarkList () .environmentObject(UserData ()) } } }
SwiftUI纲要
创建和组织视图
本教程将指导您构建一个iOS应用程序——Landmarks(地标)
,用于发现和分享您喜欢的地方。首先,您将构建显示Landmark
的详情页面。
为了布局视图,"地标"使用stacks
来对image、text
等组件进行组织和分层。要将地图添加到视图中,需要包含一个标准的MapKit
组件。当您优化视图的设计时,Xcode提供实时反馈,这样当您修改代码时,就可以看到视图状态的改变。
下载项目文件开始构建此项目,并按照以下步骤操作:
学习时长:40分钟
下载示例:CreatingAndCombiningViews.zip
第一节 创建新项目并浏览画布
创建一个使用SwiftUI的新Xcode项目。浏览画布、预览和SwiftUI模板代码。
要在Xcode中预览画布上的视图并与之交互,请确保Mac运行的是macOS Catalina 10.15。
步骤一
打开Xcode并在Xcode的启动窗口中单击Create a new Xcode
项目,或者选择File>new>project
。
步骤二
在模板选择器中,选择iOS平台,选择单视图应用程序模板,然后单击下一步。
步骤三
输入“Landmarks”作为产品名称,选中Use SwiftUI复选框,然后单击Next。选择一个路径来保存你的项目。
步骤四
在项目导航器中,单击以选择ContentView.swift。
默认情况下,SwiftUI视图文件声明两个结构。第一个结构实现了视图的必选协议,并描述了视图的内容和布局。第二个结构声明了该视图的预览。
ContentView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import SwiftUIstruct ContentView : View { var body: some View { Text ("Hello World" ) } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
步骤五
在画布中,单击Resume
以显示预览。
Tips:如果画布不可见,请选择Editor > Editor and Canvas
以显示它。
步骤六
在body
属性中,将“Hello World”更改为自己的问候语。
当您在视图的body
属性中更改代码时,预览将更新以反映您的更改。
ContentView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 import SwiftUIstruct ContentView : View { var body: some View { Text ("Hello SwiftUI!" ) } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
第二节 自定义文本视图
您可以通过更改代码,或使用检查器查看可用的内容,并帮助您编写代码,来自定义视图的显示。
构建Lanmarks
应用程序时,可以使用任意编辑器组合:源码编辑、画布或检查器。无论使用哪种工具,代码都会保持更新。
接下来,您将使用检查器自定义文本视图。
步骤一
在预览中,按住command
并单击问候语以打开结构化编辑弹出窗口,然后选择Inspect
。
弹出窗口显示可自定义的不同属性,具体取决于所检查的视图类型。
步骤二
使用检查器将文本更改为“Turtle Rock”,即您将在应用程序中显示的第一个地标的名称。
步骤三
将font
更改为Title
。
这会将系统字体应用于该文本,以便它正确适应用户系统偏好的字体大小和设置。
要自定义SwiftUI视图,可以调用名为修饰符(modifier)
的方法。修饰符会包装视图以更改其显示或其他属性。每个修饰符都返回一个新视图,因此通常采用垂直的链式调用多个修饰符。
步骤四
手动编辑代码添加foregroundColor(.green)
修饰符;这会将文本的颜色更改为绿色。
ContentView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import SwiftUIstruct ContentView : View { var body: some View { Text ("Turtle Rock" ) .font(.title) .foregroundColor(.green) } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
您的代码始终真实反应到视图上。因此当使用检查器更改或移除修饰符时,Xcode会立即更新视图以匹配代码。
步骤五
然后我们在代码编辑区,按住command
并单击Text
来打开检查器,然后从弹出窗口中选择Inspect
。单击Color
弹出菜单,然后选择Inherited
, 将文本颜色再次更改为黑色。
步骤六
请注意,Xcode会自动更新代码以反映您的更改,并删除foregroundColor(.green)
修饰符。
ContentView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import SwiftUIstruct ContentView : View { var body: some View { Text ("Turtle Rock" ) .font(.title) } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
第三节
使用堆栈组合视图
除了在上一节中创建的标题视图之外,还将添加文本视图,以包含有关地标的详细信息,例如公园的名称和所在的州。
创建SwiftUI视图时,可以在视图的body
属性中描述其内容、布局和行为;但是,body
属性只返回单个视图。您可以将多个视图组合并嵌入到Stack
中,从而将视图水平、垂直或前后组合在一起。
在本节中,您将使用VStack(垂直堆栈)
组合标题信息,使用HStack(水平堆栈)
组合公园详情信息。
你可以使用Xcode的结构化编辑功能在容器视图中嵌入视图,也可以打开Inspector
或使用help
进行其他更多有用的更改。
步骤一
按住command
并单击Text
视图,会看到初始值设定项的显示结构化编辑弹出窗口,然后选择Embed in VStack
。
接下来,通过从组件库
中拖动Text
视图,将Text
视图添加到Stack
中。
通过单击Xcode窗口右上角的加号按钮(+)打开组件库
,然后将Text
视图拖到Turtle Rock
文本视图下面的位置。
步骤三
将Text
视图的默认文本替换为“Joshua Tree National Park”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import SwiftUIstruct ContentView : View { var body: some View { VStack { Text ("Turtle Rock" ) .font(.title) Text ("Joshua Tree National Park" ) } } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
自定义位置文本的样式,以满足布局需求。
步骤四
将位置的font
设置为subheadline
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import SwiftUIstruct ContentView : View { var body: some View { VStack { Text ("Turtle Rock" ) .font(.title) Text ("Joshua Tree National Park" ) .font(.subheadline) } } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
步骤五
编辑VStack
初始值设定项,以leading
方式对齐视图。
默认情况下,VStack
会将它们的内容按轴中心对齐,并提供上下文适当的间距。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import SwiftUIstruct ContentView : View { var body: some View { VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) Text ("Joshua Tree National Park" ) .font(.subheadline) } } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
接下来,您将在位置文本的右侧添加另一个文本视图,该视图用于展示公园所在的州。
步骤六
在画布中,按住command
并单击Joshua Tree National Park
,然后选择Embed In HStack
。
步骤七
在地点文本后添加新的文本视图,将默认文本更改为公园所在的州,然后将其font
设置为subheadline
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct ContentView : View { var body: some View { VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) HStack { Text ("Joshua Tree National Park" ) .font(.subheadline) Text ("California" ) .font(.subheadline) } } } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
步骤八
若想使布局自动撑开并充满设备的宽,在HStack
中添加Spacer
来分隔Joshua Tree National Park
和California
两个文本视图。
Spacer
将撑开Stack
,以使其包含的视图充满其父视图的所有空间,而不是仅由其内容定义其大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import SwiftUIstruct ContentView : View { var body: some View { VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) HStack { Text ("Joshua Tree National Park" ) .font(.subheadline) Spacer () Text ("California" ) .font(.subheadline) } } } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
步骤九
最后,使用padding()
修饰符方法给整个地标信息区域加一个边距。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import SwiftUIstruct ContentView : View { var body: some View { VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) HStack { Text ("Joshua Tree National Park" ) .font(.subheadline) Spacer () Text ("California" ) .font(.subheadline) } } .padding() } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
第四节
创建自定义图像视图
在名称和地点视图都设置好的情况下,接下来要做的是为地标添加一个图像。
对自定义的图像使用遮罩、边框和阴影,和原来的方法相比,将使用很少的代码。
首先将图片添加到项目的Assets
中。
步骤一
在项目Resources
文件夹的中找到turtlerock.png
,将其拖到Assets
中。Xcode为图像创建一个新的图像集。
接下来,您将为您的自定义图像视图创建一个新的SwiftUI视图。
步骤二
选择File > New > File
再次打开模板选择器。在User Interface
中,单击选择SwiftUI View
,然后单击Next
。将文件命名为CircleImage.swift
,然后单击Create
。
现在您已准备好图像,以满足设计所需。
步骤三
使用Image(:)
将Text
视图替换为Turtle Rock的图像视图。
CircleImage.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 import SwiftUIstruct CircleImage : View { var body: some View { Image ("turtlerock" ) } } struct CircleImage_Preview : PreviewProvider { static var previews: some View { CircleImage () } }
步骤四
添加Image
的.clipShape(Circle())
的修饰符,将图像剪裁为圆形。
Circle()
是一个可以用作遮罩的形状,或通过给Circle()
设置stroke
或fill
来绘制视图。
CircleImage.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import SwiftUIstruct CircleImage : View { var body: some View { Image ("turtlerock" ) .clipShape(Circle ()) } } struct CircleImage_Preview : PreviewProvider { static var previews: some View { CircleImage () } }
步骤五
创建另一个带有灰色stroke
的Circle()
,使用.overlay()
修饰符,将其覆盖添加到图像的边框中。
CircleImage.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import SwiftUIstruct CircleImage : View { var body: some View { Image ("turtlerock" ) .clipShape(Circle ()) .overlay( Circle ().stroke(Color .gray, lineWidth: 4 )) } } struct CircleImage_Preview : PreviewProvider { static var previews: some View { CircleImage () } }
步骤六
接下来,添加半径为10point
的阴影。
CircleImage.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import SwiftUIstruct CircleImage : View { var body: some View { Image ("turtlerock" ) .clipShape(Circle ()) .overlay( Circle ().stroke(Color .gray, lineWidth: 4 )) .shadow(radius: 10 ) } } struct CircleImage_Preview : PreviewProvider { static var previews: some View { CircleImage () } }
步骤七
将边框颜色修改为白色。
到此,我们就完成了图像视图。
CircleImage.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import SwiftUIstruct CircleImage : View { var body: some View { Image ("turtlerock" ) .clipShape(Circle ()) .overlay( Circle ().stroke(Color .white, lineWidth: 4 )) .shadow(radius: 10 ) } } struct CircleImage_Preview : PreviewProvider { static var previews: some View { CircleImage () } }
第五节
UIKit和SwiftUI配合使用
现在可以创建地图视图了,您可以使用MapKit
中的MKMappView
类来绘制地图。
要在SwiftUI中使用UIView子类,可以将另一个视图包装在遵守UIViewRepresentable协议的SwiftUI视图中。SwiftUI包含了WatchKit和AppKit视图类似的协议。
现在开始,您将创建一个新的自定义视图,该视图可以显示MKMapView
。
选择File > New > File
,选择iOS
平台,选择SwiftUI View
模板,然后单击Next
。将新文件命名为MapView.swift
,然后单击“Create”。
步骤二
添加import MapKit
,并让MappView
结构遵守UIViewRepresentable协议。
不要担心Xcode显示的警告;您将在接下来的几个步骤中修复它。
MapView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import SwiftUIimport MapKitstruct MapView : UIViewRepresentable { var body: some View { Text ("Hello World" ) } } struct MapView_Preview : PreviewProvider { static var previews: some View { MapView () } }
UIViewRepresentable
协议有两个需要实现的方法:使用UIView(context:)
方法创建MKMapView
,使用updateUIView(u:context:)
方法来配置视图并响应视图的任意变化。
步骤三
用makeUIView(context:)
方法替换body属性。
MapView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import SwiftUIimport MapKitstruct MapView : UIViewRepresentable { func makeUIView (context : Context ) -> MKMapView { MKMapView (frame: .zero) } } struct MapView_Preview : PreviewProvider { static var previews: some View { MapView () } }
步骤四
创建一个updateUIView(u:context:)
方法,将地图视图的区域设置为正确的坐标,以便将地图居中放置在Turtle Rock
上。
MapView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import SwiftUI import MapKit struct MapView: UIViewRepresentable { func makeUIView(context: Context) -> MKMapView { MKMapView(frame: .zero) } func updateUIView(_ view: MKMapView, context: Context) { let coordinate = CLLocationCoordinate2D( latitude: 34.011286, longitude: -116.166868) let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0) let region = MKCoordinateRegion(center: coordinate, span: span) view.setRegion(region, animated: true) } } struct MapView_Preview: PreviewProvider { static var previews: some View { MapView() } }
当预览处于静态模式时,它们仅完全呈现SwiftUI
视图。因为MKMapView
是一个UIView
子类,所以您需要切换到实时预览来查看地图。
步骤五
单击Live Preview
按钮将预览切换到实时预览模式。您可能需要单击预览上方的Try Again
或Resume
按钮。
再过一会儿,你会看到一张Joshua Tree National Park
的地图,那里是Turtle Rock
的故乡。
第六节
组成详情视图
现在您已经拥有了所需的所有组件——名称和位置、圆形图像以及地图。
现在使用目前的工具,组合自定义视图,创建地标详情视图以达到最终设计吧。
步骤一
在项目导航器中,选择ContentView.swift
文件。
ContentView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import SwiftUIstruct ContentView : View { var body: some View { VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) HStack { Text ("Joshua Tree National Park" ) .font(.subheadline) Spacer () Text ("California" ) .font(.subheadline) } } .padding() } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
步骤二
将刚才的三个文本的VStack嵌入到另一个VStack中。
ContentView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import SwiftUIstruct ContentView : View { var body: some View { VStack { VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) HStack (alignment: .top) { Text ("Joshua Tree National Park" ) .font(.subheadline) Spacer () Text ("California" ) .font(.subheadline) } } .padding() } } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
步骤三
将自定义MapView
添加到Stack
顶部。使用frame(width:height:)
设置MapView
的大小。
仅指定height(高度)
参数时,视图会自动调整其内容的宽度。在这种情况下,MapView
将展开以填充可用空间。
步骤四
单击Live Preview
按钮,以在预览中查看渲染的地图。
您可以在显示实时预览时继续编辑视图。
ContentView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import SwiftUIstruct ContentView : View { var body: some View { VStack { MapView () .frame(height: 300 ) VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) HStack (alignment: .top) { Text ("Joshua Tree National Park" ) .font(.subheadline) Spacer () Text ("California" ) .font(.subheadline) } } .padding() } } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
步骤五
将CircleImage
视图添加到Stack
中。
ContentView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import SwiftUIstruct ContentView : View { var body: some View { VStack { MapView () .frame(height: 300 ) CircleImage () VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) HStack (alignment: .top) { Text ("Joshua Tree National Park" ) .font(.subheadline) Spacer () Text ("California" ) .font(.subheadline) } } .padding() } } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
步骤六
若要将CircleImage
显示在MapView
视图的上方,请使图像垂直偏移-130个点,并从视图底部填充-130个点。
这些调整通过向上移动图像为文本腾出空间。
ContentView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import SwiftUIstruct ContentView : View { var body: some View { VStack { MapView () .frame(height: 300 ) CircleImage () .offset(y: - 130 ) .padding(.bottom, - 130 ) VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) HStack (alignment: .top) { Text ("Joshua Tree National Park" ) .font(.subheadline) Spacer () Text ("California" ) .font(.subheadline) } } .padding() } } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
步骤七
在VStack
中底部添加一个Spacer()
,将内容推到屏幕的顶部。
ContentView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import SwiftUIstruct ContentView : View { var body: some View { VStack { MapView () .frame(height: 300 ) CircleImage () .offset(y: - 130 ) .padding(.bottom, - 130 ) VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) HStack (alignment: .top) { Text ("Joshua Tree National Park" ) .font(.subheadline) Spacer () Text ("California" ) .font(.subheadline) } } .padding() Spacer () } } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
步骤八
最后,要允许地图内容扩展到屏幕的上边缘,请将edgesIgnoringSafeArea(.top)
修饰符添加到地图视图中。
ContentView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import SwiftUIstruct ContentView : View { var body: some View { VStack { MapView () .edgesIgnoringSafeArea(.top) .frame(height: 300 ) CircleImage () .offset(y: - 130 ) .padding(.bottom, - 130 ) VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) HStack (alignment: .top) { Text ("Joshua Tree National Park" ) .font(.subheadline) Spacer () Text ("California" ) .font(.subheadline) } } .padding() Spacer () } } } struct ContentView_Preview : PreviewProvider { static var previews: some View { ContentView () } }
绘图和动画
绘制路径和形状
用户每次访问列表中的地标时都会收到徽章。当然,要让用户收到徽章,您需要创建一个徽章。本教程将引导您通过组合路径和形状来创建徽章,然后将其与表示位置的另一个形状重叠。
如果要为不同类型的地标创建多个徽章,请尝试使用覆盖的符号、更改重复次数或更改不同角度和比例。
学习时间:25分钟
下载地址:DrawingPathsAndShapes.zip
第一节 创建徽章视图
要创建徽章,首先要创建一个徽章视图,该视图使用SwiftUI
中的矢量绘图api
。
步骤1
选择File > New > File
,然后从“iOS模板”工作表中选择SwiftUI View
。单击Next
继续,然后在“Save as”字段中输入Badge
并单击“Create”。
步骤2
调整Badge
视图以显示文本“Badge”,接下来我们开始定义徽章形状。
Badge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 import SwiftUIstruct Badge : View { var body: some View { Text ("Badge" ) } } struct Badge_Previews : PreviewProvider { static var previews: some View { Badge () } }
第二节 绘制徽章背景
使用SwiftUI中的图形api绘制自定义徽章形状。
步骤1
查看HexagonParameters.swift
文件中的代码。
六边形参数结构定义了绘制徽章六边形的详细信息。您不会修改此数据;相反,您将使用它指定用于绘制徽章的线条和曲线的控制点。
HexagonParameters.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import SwiftUIstruct HexagonParameters { struct Segment { let useWidth: (CGFloat , CGFloat , CGFloat ) let xFactors: (CGFloat , CGFloat , CGFloat ) let useHeight: (CGFloat , CGFloat , CGFloat ) let yFactors: (CGFloat , CGFloat , CGFloat ) } static let adjustment: CGFloat = 0.085 static let points = [ Segment ( useWidth: (1.00 , 1.00 , 1.00 ), xFactors: (0.60 , 0.40 , 0.50 ), useHeight: (1.00 , 1.00 , 0.00 ), yFactors: (0.05 , 0.05 , 0.00 ) ), Segment ( useWidth: (1.00 , 1.00 , 0.00 ), xFactors: (0.05 , 0.00 , 0.00 ), useHeight: (1.00 , 1.00 , 1.00 ), yFactors: (0.20 + adjustment, 0.30 + adjustment, 0.25 + adjustment) ), Segment ( useWidth: (1.00 , 1.00 , 0.00 ), xFactors: (0.00 , 0.05 , 0.00 ), useHeight: (1.00 , 1.00 , 1.00 ), yFactors: (0.70 - adjustment, 0.80 - adjustment, 0.75 - adjustment) ), Segment ( useWidth: (1.00 , 1.00 , 1.00 ), xFactors: (0.40 , 0.60 , 0.50 ), useHeight: (1.00 , 1.00 , 1.00 ), yFactors: (0.95 , 0.95 , 1.00 ) ), Segment ( useWidth: (1.00 , 1.00 , 1.00 ), xFactors: (0.95 , 1.00 , 1.00 ), useHeight: (1.00 , 1.00 , 1.00 ), yFactors: (0.80 - adjustment, 0.70 - adjustment, 0.75 - adjustment) ), Segment ( useWidth: (1.00 , 1.00 , 1.00 ), xFactors: (1.00 , 0.95 , 1.00 ), useHeight: (1.00 , 1.00 , 1.00 ), yFactors: (0.30 + adjustment, 0.20 + adjustment, 0.25 + adjustment) ) ] }
步骤2
在Badge.swift
中,向Badge
添加一个Path shape
,并应用fill()
修饰符将该形状转换为视图。
您可以使用paths
来组合线条、曲线和其他绘图,以形成更复杂的形状,如徽章的六边形背景。
Badge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import SwiftUIstruct Badge : View { var body: some View { Path { path in } .fill(Color .black) } } struct Badge_Previews : PreviewProvider { static var previews: some View { Badge () } }
步骤3
为path
添加起点。
move(to:)
方法在形状的边界内移动绘图光标,就像一支虚构的笔悬停在该区域上,等待开始绘图一样。
Badge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import SwiftUIstruct Badge : View { var body: some View { Path { path in var width: CGFloat = 100.0 let height = width path.move(to: CGPoint (x: width * 0.95 , y: height * 0.20 )) } .fill(Color .black) } } struct Badge_Previews : PreviewProvider { static var previews: some View { Badge () } }
步骤4
为形状数据的每个点绘制线,以创建一个大致的六边形。
addLine(to:)
方法接受一个点并绘制它。对addLine(to:)
连续调用,会从上一点开始到新点画一条线。
Badge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import SwiftUIstruct Badge : View { var body: some View { Path { path in var width: CGFloat = 100.0 let height = width path.move(to: CGPoint (x: width * 0.95 , y: height * 0.20 )) HexagonParameters .points.forEach { path.addLine( to: .init ( x: width * $0 .useWidth.0 * $0 .xFactors.0 , y: height * $0 .useHeight.0 * $0 .yFactors.0 ) ) } } .fill(Color .black) } } struct Badge_Previews : PreviewProvider { static var previews: some View { Badge () } }
如果你的六边形看起来有点不寻常,不要担心;这符合预期。在接下来的几个步骤中,您将努力使六边形看起来更像本节开头显示的徽章形状。
步骤5
使用addQuadCurve(to:control:)
方法为徽章的角绘制Bézier
曲线。
Badge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import SwiftUIstruct Badge : View { var body: some View { Path { path in var width: CGFloat = 100.0 let height = width path.move( to: CGPoint ( x: width * 0.95 , y: height * (0.20 + HexagonParameters .adjustment) ) ) HexagonParameters .points.forEach { path.addLine( to: .init ( x: width * $0 .useWidth.0 * $0 .xFactors.0 , y: height * $0 .useHeight.0 * $0 .yFactors.0 ) ) path.addQuadCurve( to: .init ( x: width * $0 .useWidth.1 * $0 .xFactors.1 , y: height * $0 .useHeight.1 * $0 .yFactors.1 ), control: .init ( x: width * $0 .useWidth.2 * $0 .xFactors.2 , y: height * $0 .useHeight.2 * $0 .yFactors.2 ) ) } } .fill(Color .black) } } struct Badge_Previews : PreviewProvider { static var previews: some View { Badge () } }
步骤6
将徽章path
包装在GeometryReader
中,以便徽章可以使用其包含视图的大小,该视图定义大小,而不是硬编码值(100)。
当包含的视图不是正方形时,使用几何体的两个维度中的最小值以保证徽章的宽高比。
Badge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import SwiftUIstruct Badge : View { var body: some View { GeometryReader { geometry in Path { path in var width: CGFloat = min (geometry.size.width, geometry.size.height) let height = width path.move( to: CGPoint ( x: width * 0.95 , y: height * (0.20 + HexagonParameters .adjustment) ) ) HexagonParameters .points.forEach { path.addLine( to: .init ( x: width * $0 .useWidth.0 * $0 .xFactors.0 , y: height * $0 .useHeight.0 * $0 .yFactors.0 ) ) path.addQuadCurve( to: .init ( x: width * $0 .useWidth.1 * $0 .xFactors.1 , y: height * $0 .useHeight.1 * $0 .yFactors.1 ), control: .init ( x: width * $0 .useWidth.2 * $0 .xFactors.2 , y: height * $0 .useHeight.2 * $0 .yFactors.2 ) ) } } .fill(Color .black) } } } struct Badge_Previews : PreviewProvider { static var previews: some View { Badge () } }
步骤7
使用xScale
和xOffset
调整变量使徽章在其几何体中居中。
Badge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import SwiftUIstruct Badge : View { var body: some View { GeometryReader { geometry in Path { path in var width: CGFloat = min (geometry.size.width, geometry.size.height) let height = width let xScale: CGFloat = 0.832 let xOffset = (width * (1.0 - xScale)) / 2.0 width *= xScale path.move( to: CGPoint ( x: xOffset + width * 0.95 , y: height * (0.20 + HexagonParameters .adjustment) ) ) HexagonParameters .points.forEach { path.addLine( to: .init ( x: xOffset + width * $0 .useWidth.0 * $0 .xFactors.0 , y: height * $0 .useHeight.0 * $0 .yFactors.0 ) ) path.addQuadCurve( to: .init ( x: xOffset + width * $0 .useWidth.1 * $0 .xFactors.1 , y: height * $0 .useHeight.1 * $0 .yFactors.1 ), control: .init ( x: xOffset + width * $0 .useWidth.2 * $0 .xFactors.2 , y: height * $0 .useHeight.2 * $0 .yFactors.2 ) ) } } .fill(Color .black) } } } struct Badge_Previews : PreviewProvider { static var previews: some View { Badge () } }
步骤8
将徽章的纯黑背景替换为与设计匹配的渐变色。
Badge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import SwiftUIstruct Badge : View { var body: some View { GeometryReader { geometry in Path { path in var width: CGFloat = min (geometry.size.width, geometry.size.height) let height = width let xScale: CGFloat = 0.832 let xOffset = (width * (1.0 - xScale)) / 2.0 width *= xScale path.move( to: CGPoint ( x: xOffset + width * 0.95 , y: height * (0.20 + HexagonParameters .adjustment) ) ) HexagonParameters .points.forEach { path.addLine( to: .init ( x: xOffset + width * $0 .useWidth.0 * $0 .xFactors.0 , y: height * $0 .useHeight.0 * $0 .yFactors.0 ) ) path.addQuadCurve( to: .init ( x: xOffset + width * $0 .useWidth.1 * $0 .xFactors.1 , y: height * $0 .useHeight.1 * $0 .yFactors.1 ), control: .init ( x: xOffset + width * $0 .useWidth.2 * $0 .xFactors.2 , y: height * $0 .useHeight.2 * $0 .yFactors.2 ) ) } } .fill(LinearGradient ( gradient: .init (colors: [Self .gradientStart, Self .gradientEnd]), startPoint: .init (x: 0.5 , y: 0 ), endPoint: .init (x: 0.5 , y: 0.6 ) )) } } static let gradientStart = Color (red: 239.0 / 255 , green: 120.0 / 255 , blue: 221.0 / 255 ) static let gradientEnd = Color (red: 239.0 / 255 , green: 172.0 / 255 , blue: 120.0 / 255 ) } struct Badge_Previews : PreviewProvider { static var previews: some View { Badge () } }
步骤9
对渐变填充应用aspectRatio(u:contentMode:)
修饰符。
即使其父视图不是正方形的,可以通过它可以使视图保持1:1的宽高比,并保持其在视图中心的位置。
Badge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import SwiftUIstruct Badge : View { var body: some View { GeometryReader { geometry in Path { path in var width: CGFloat = min (geometry.size.width, geometry.size.height) let height = width let xScale: CGFloat = 0.832 let xOffset = (width * (1.0 - xScale)) / 2.0 width *= xScale path.move( to: CGPoint ( x: xOffset + width * 0.95 , y: height * (0.20 + HexagonParameters .adjustment) ) ) HexagonParameters .points.forEach { path.addLine( to: .init ( x: xOffset + width * $0 .useWidth.0 * $0 .xFactors.0 , y: height * $0 .useHeight.0 * $0 .yFactors.0 ) ) path.addQuadCurve( to: .init ( x: xOffset + width * $0 .useWidth.1 * $0 .xFactors.1 , y: height * $0 .useHeight.1 * $0 .yFactors.1 ), control: .init ( x: xOffset + width * $0 .useWidth.2 * $0 .xFactors.2 , y: height * $0 .useHeight.2 * $0 .yFactors.2 ) ) } } .fill(LinearGradient ( gradient: .init (colors: [Self .gradientStart, Self .gradientEnd]), startPoint: .init (x: 0.5 , y: 0 ), endPoint: .init (x: 0.5 , y: 0.6 ) )) .aspectRatio(1 , contentMode: .fit) } } static let gradientStart = Color (red: 239.0 / 255 , green: 120.0 / 255 , blue: 221.0 / 255 ) static let gradientEnd = Color (red: 239.0 / 255 , green: 172.0 / 255 , blue: 120.0 / 255 ) } struct Badge_Previews : PreviewProvider { static var previews: some View { Badge () } }
第三节 画徽章符号
地标徽章的中心有一个自定义徽章,该徽章基于地标应用程序图标中显示的山。
山体符号由两个形状组成:一个表示山顶的积雪,另一个表示沿引道的植被。您将使用两个由一个小间隙隔开的部分三角形来绘制它们。
步骤1
在名为BadgeBackground.swift
的新文件中,将徽章视图的主体放入新的徽章背景视图中,以便为其他视图准备徽章视图。
BadgeBackground.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import SwiftUIstruct BadgeBackground : View { var body: some View { GeometryReader { geometry in Path { path in var width: CGFloat = min (geometry.size.width, geometry.size.height) let height = width let xScale: CGFloat = 0.832 let xOffset = (width * (1.0 - xScale)) / 2.0 width *= xScale path.move( to: CGPoint ( x: xOffset + width * 0.95 , y: height * (0.20 + HexagonParameters .adjustment) ) ) HexagonParameters .points.forEach { path.addLine( to: .init ( x: xOffset + width * $0 .useWidth.0 * $0 .xFactors.0 , y: height * $0 .useHeight.0 * $0 .yFactors.0 ) ) path.addQuadCurve( to: .init ( x: xOffset + width * $0 .useWidth.1 * $0 .xFactors.1 , y: height * $0 .useHeight.1 * $0 .yFactors.1 ), control: .init ( x: xOffset + width * $0 .useWidth.2 * $0 .xFactors.2 , y: height * $0 .useHeight.2 * $0 .yFactors.2 ) ) } } .fill(LinearGradient ( gradient: .init (colors: [Self .gradientStart, Self .gradientEnd]), startPoint: .init (x: 0.5 , y: 0 ), endPoint: .init (x: 0.5 , y: 0.6 ) )) .aspectRatio(1 , contentMode: .fit) } } static let gradientStart = Color (red: 239.0 / 255 , green: 120.0 / 255 , blue: 221.0 / 255 ) static let gradientEnd = Color (red: 239.0 / 255 , green: 172.0 / 255 , blue: 120.0 / 255 ) } struct BadgeBackground_Previews : PreviewProvider { static var previews: some View { BadgeBackground () } }
步骤2
将徽章背景放在徽章主体中以恢复徽章。
Badge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 import SwiftUIstruct Badge : View { var body: some View { BadgeBackground () } } struct Badge_Previews : PreviewProvider { static var previews: some View { Badge () } }
步骤3
为山形创建一个名为BadgeSymbol
的新自定义视图,该山形在徽章设计中以旋转方式绘制。
BadgeSymbol.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 import SwiftUIstruct BadgeSymbol : View { var body: some View { Text ("Badge Symbol" ) } } struct BadgeSymbol_Previews : PreviewProvider { static var previews: some View { BadgeSymbol () } }
步骤4
使用Paths api
绘制符号的顶部。
实验
尝试调整与spacing、topWidth和topHeight常量关联的系数,以查看它们如何影响整体形状。
BadgeSymbol.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import SwiftUIstruct BadgeSymbol : View { var body: some View { GeometryReader { geometry in Path { path in let width = min (geometry.size.width, geometry.size.height) let height = width * 0.75 let spacing = width * 0.030 let middle = width / 2 let topWidth = 0.226 * width let topHeight = 0.488 * height path.addLines([ CGPoint (x: middle, y: spacing), CGPoint (x: middle - topWidth, y: topHeight - spacing), CGPoint (x: middle, y: topHeight / 2 + spacing), CGPoint (x: middle + topWidth, y: topHeight - spacing), CGPoint (x: middle, y: spacing) ]) } } } } struct BadgeSymbol_Previews : PreviewProvider { static var previews: some View { BadgeSymbol () } }
步骤5
绘制符号的底部。
使用move(to:)
修饰符在同一路径中的多个形状之间插入间隙。
BadgeSymbol.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import SwiftUIstruct BadgeSymbol : View { var body: some View { GeometryReader { geometry in Path { path in let width = min (geometry.size.width, geometry.size.height) let height = width * 0.75 let spacing = width * 0.030 let middle = width / 2 let topWidth = 0.226 * width let topHeight = 0.488 * height path.addLines([ CGPoint (x: middle, y: spacing), CGPoint (x: middle - topWidth, y: topHeight - spacing), CGPoint (x: middle, y: topHeight / 2 + spacing), CGPoint (x: middle + topWidth, y: topHeight - spacing), CGPoint (x: middle, y: spacing) ]) path.move(to: CGPoint (x: middle, y: topHeight / 2 + spacing * 3 )) path.addLines([ CGPoint (x: middle - topWidth, y: topHeight + spacing), CGPoint (x: spacing, y: height - spacing), CGPoint (x: width - spacing, y: height - spacing), CGPoint (x: middle + topWidth, y: topHeight + spacing), CGPoint (x: middle, y: topHeight / 2 + spacing * 3 ) ]) } } } } struct BadgeSymbol_Previews : PreviewProvider { static var previews: some View { BadgeSymbol () } }
步骤6
用设计中的紫色填充符号。
BadgeSymbol.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import SwiftUIstruct BadgeSymbol : View { static let symbolColor = Color (red: 79.0 / 255 , green: 79.0 / 255 , blue: 191.0 / 255 ) var body: some View { GeometryReader { geometry in Path { path in let width = min (geometry.size.width, geometry.size.height) let height = width * 0.75 let spacing = width * 0.030 let middle = width / 2 let topWidth = 0.226 * width let topHeight = 0.488 * height path.addLines([ CGPoint (x: middle, y: spacing), CGPoint (x: middle - topWidth, y: topHeight - spacing), CGPoint (x: middle, y: topHeight / 2 + spacing), CGPoint (x: middle + topWidth, y: topHeight - spacing), CGPoint (x: middle, y: spacing) ]) path.move(to: CGPoint (x: middle, y: topHeight / 2 + spacing * 3 )) path.addLines([ CGPoint (x: middle - topWidth, y: topHeight + spacing), CGPoint (x: spacing, y: height - spacing), CGPoint (x: width - spacing, y: height - spacing), CGPoint (x: middle + topWidth, y: topHeight + spacing), CGPoint (x: middle, y: topHeight / 2 + spacing * 3 ) ]) } .fill(Self .symbolColor) } } } struct BadgeSymbol_Previews : PreviewProvider { static var previews: some View { BadgeSymbol () } }
第四节 结合徽章前景和背景
徽章设计要求在徽章背景上旋转和重复多次山形。
定义一种新的rotation
类型,并利用ForEach
视图对山形的多个副本使用相同的调整。
步骤1
创建新的RotatedBadgeSymbol
视图以封装旋转符号。
实验
调整预览中的角度以测试旋转的效果。
RotatedBadgeSymbol.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import SwiftUIstruct RotatedBadgeSymbol : View { let angle: Angle var body: some View { BadgeSymbol () .padding(- 60 ) .rotationEffect(angle, anchor: .bottom) } } struct RotatedBadgeSymbol_Previews : PreviewProvider { static var previews: some View { RotatedBadgeSymbol (angle: .init (degrees: 5 )) } }
步骤2
在Badge.swift
中,将徽章的符号放置在ZStack
中,将其放置在徽章背景上。
Badge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import SwiftUIstruct Badge : View { var badgeSymbols: some View { RotatedBadgeSymbol (angle: .init (degrees: 0 )) .opacity(0.5 ) } var body: some View { ZStack { BadgeBackground () self .badgeSymbols } } } struct Badge_Previews : PreviewProvider { static var previews: some View { Badge () } }
现在看来,徽章符号与预期的设计和背景的相对大小相比太大了。
步骤3
通过读取周围的几何图形并缩放符号,更正徽章符号的大小。
Badge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import SwiftUIstruct Badge : View { var badgeSymbols: some View { RotatedBadgeSymbol (angle: .init (degrees: 0 )) .opacity(0.5 ) } var body: some View { ZStack { BadgeBackground () GeometryReader { geometry in self .badgeSymbols .scaleEffect(1.0 / 4.0 , anchor: .top) .position(x: geometry.size.width / 2.0 , y: (3.0 / 4.0 ) * geometry.size.height) } } } } struct Badge_Previews : PreviewProvider { static var previews: some View { Badge () } }
步骤4
使用ForEach
创建旋转显示多个徽章符号的副本。
一个完整的360°旋转分成八个部分,通过重复山脉符号创建一个类似太阳的图案。
Badge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import SwiftUIstruct Badge : View { static let rotationCount = 8 var badgeSymbols: some View { ForEach (0 ..< Badge .rotationCount) { i in RotatedBadgeSymbol ( angle: .degrees(Double (i) / Double (Badge .rotationCount)) * 360.0 ) } .opacity(0.5 ) } var body: some View { ZStack { BadgeBackground () GeometryReader { geometry in self .badgeSymbols .scaleEffect(1.0 / 4.0 , anchor: .top) .position(x: geometry.size.width / 2.0 , y: (3.0 / 4.0 ) * geometry.size.height) } } .scaledToFit() } } struct Badge_Previews : PreviewProvider { static var previews: some View { Badge () } }
框架集成
与UIKit协作
SwiftUI与所有苹果平台上现有的UI框架无缝协作。例如,可以将UIKit视图和视图控制器放置在SwiftUI视图中,反之亦然。
本教程向您展示如何将主页的特色地标转换为包装UIPageViewController和UIPageControl实例。您将使用UIPageViewController来显示SwiftUI视图的轮播,并使用状态变量和绑定来协调整个用户界面中的数据更新。
学习时间:25分钟
下载地址:InterfacingWithUIKit.zip
第一节
创建显示UIPageViewController的视图
要在SwiftUI中显示UIKit视图和视图控制器,可以创建遵守UIViewRepresentable
和UIViewControllerRepresentable
协议的类型,您的自定义类型创建并配置它们所表示的UIKit类型,而SwiftUI管理它们的生命周期,并在需要时更新它们。
步骤1
创建一个名为PageViewController.swift
的新SwiftUI视图文件,并声明PageViewController
类型遵守UIViewControllerRepresentable
。
页面视图控制器存储UIViewController
实例的数组。这些是在地标之间滚动的页面。
PageViewController.swift
1 2 3 4 5 6 import SwiftUIimport UIKitstruct PageViewController : UIViewControllerRepresentable { var controllers: [UIViewController ] }
接下来,添加UIViewControllerRepresentable
协议的两个方法。
步骤2
添加makeUIViewController(context:)
方法,该方法创建满足需求的UIPageViewController
。
SwiftUI在准备好显示视图时调用此方法一次,然后管理视图控制器的生命周期。
PageViewController.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import SwiftUIimport UIKitstruct PageViewController : UIViewControllerRepresentable { var controllers: [UIViewController ] func makeUIViewController (context : Context ) -> UIPageViewController { let pageViewController = UIPageViewController ( transitionStyle: .scroll, navigationOrientation: .horizontal) return pageViewController } }
步骤3
添加一个updateUIViewController(_:context:)
方法,该方法调用setViewControllers(_:direction:animated:)
以显示数组中的第一个视图控制器。
PageViewController.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import SwiftUIimport UIKitstruct PageViewController : UIViewControllerRepresentable { var controllers: [UIViewController ] func makeUIViewController (context : Context ) -> UIPageViewController { let pageViewController = UIPageViewController ( transitionStyle: .scroll, navigationOrientation: .horizontal) return pageViewController } func updateUIViewController (_ pageViewController : UIPageViewController , context : Context ) { pageViewController.setViewControllers( [controllers[0 ]], direction: .forward, animated: true ) } }
创建另一个SwiftUI视图以显示UIViewControllerRepresentable视图。
步骤4
创建一个名为PageView.swift
的新SwiftUI视图文件,并更新PageView
类型以将PageViewController
声明为子视图。
注意,泛型初始值设定项接受一个视图数组,并将每个视图嵌套在UIHostingController
中。UIHostingController
是UIViewController
子类,表示UIKit上下文中的SwiftUI视图。
PageView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import SwiftUIstruct PageView <Page : View >: View { var viewControllers: [UIHostingController <Page >] init (_ views : [Page ]) { self .viewControllers = views.map { UIHostingController (rootView: $0 ) } } var body: some View { PageViewController (controllers: viewControllers) } } struct PageView_Preview : PreviewProvider { static var previews: some View { PageView () } }
步骤5
更新PageView_Preview
以传递所需的视图数组,此时预览开始工作。
PageView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import SwiftUIstruct PageView <Page : View >: View { var viewControllers: [UIHostingController <Page >] init (_ views : [Page ]) { self .viewControllers = views.map { UIHostingController (rootView: $0 ) } } var body: some View { PageViewController (controllers: viewControllers) } } struct PageView_Preview : PreviewProvider { static var previews: some View { PageView (features.map { FeatureCard (landmark: $0 ) }) .aspectRatio(3 / 2 , contentMode: .fit) } }
步骤6
在继续下一步之前,将页面视图预览固定到画布,所有操作变化都会发生在此视图上。
第二节 创建视图控制器的数据源
在几个简单的步骤中,您已经完成了很多工作—PageViewController
使用一个UIPageViewController
在SwiftUI视图中显示内容。现在是时候让视图轮播滚动了。
代表UIKit视图控制器的SwiftUI视图可以定义一个``类型,SwiftUI将其作为可表示视图上下文的一部分进行提供和管理。
步骤1
在PageViewController
中声明嵌套的Coordinator
类。
SwiftUI管理UIViewControllerRepresentable
类型的Coordinator
,并在调用上面创建的方法时将其作为上下文的一部分提供。
PageViewController.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import SwiftUIimport UIKitstruct PageViewController : UIViewControllerRepresentable { var controllers: [UIViewController ] func makeUIViewController (context : Context ) -> UIPageViewController { let pageViewController = UIPageViewController ( transitionStyle: .scroll, navigationOrientation: .horizontal) return pageViewController } func updateUIViewController (_ pageViewController : UIPageViewController , context : Context ) { pageViewController.setViewControllers( [controllers[0 ]], direction: .forward, animated: true ) } class Coordinator : NSObject { var parent: PageViewController init (_ pageViewController : PageViewController ) { self .parent = pageViewController } } }
步骤2
向PageViewController
添加另一个方法以生成Coordinator
。
SwiftUI在makeUIViewController(context:)
之前调用此makeCoordinator()
方法,以便在配置视图控制器时可以访问coordinator
对象。
Tips 您可以使用这个Coordinator
来实现常见的Cocoa
模式,例如委托、数据源和通过target-action
响应用户事件。
PageViewController.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import SwiftUIimport UIKitstruct PageViewController : UIViewControllerRepresentable { var controllers: [UIViewController ] func makeCoordinator () -> Coordinator { Coordinator (self ) } func makeUIViewController (context : Context ) -> UIPageViewController { let pageViewController = UIPageViewController ( transitionStyle: .scroll, navigationOrientation: .horizontal) return pageViewController } func updateUIViewController (_ pageViewController : UIPageViewController , context : Context ) { pageViewController.setViewControllers( [controllers[0 ]], direction: .forward, animated: true ) } class Coordinator : NSObject { var parent: PageViewController init (_ pageViewController : PageViewController ) { self .parent = pageViewController } } }
步骤3
向Coordinator
类型添加UIPageViewControllerDataSource
一致性,并实现两个必需的方法。
这两个方法建立视图控制器之间的关系,以便您可以在它们之间来回滑动。
PageViewController.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import SwiftUIimport UIKitstruct PageViewController : UIViewControllerRepresentable { var controllers: [UIViewController ] func makeCoordinator () -> Coordinator { Coordinator (self ) } func makeUIViewController (context : Context ) -> UIPageViewController { let pageViewController = UIPageViewController ( transitionStyle: .scroll, navigationOrientation: .horizontal) return pageViewController } func updateUIViewController (_ pageViewController : UIPageViewController , context : Context ) { pageViewController.setViewControllers( [controllers[0 ]], direction: .forward, animated: true ) } class Coordinator : NSObject , UIPageViewControllerDataSource { var parent: PageViewController init (_ pageViewController : PageViewController ) { self .parent = pageViewController } func pageViewController ( _ pageViewController : UIPageViewController , viewControllerBefore viewController : UIViewController ) -> UIViewController ? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index == 0 { return parent.controllers.last } return parent.controllers[index - 1 ] } func pageViewController ( _ pageViewController : UIPageViewController , viewControllerAfter viewController : UIViewController ) -> UIViewController ? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index + 1 == parent.controllers.count { return parent.controllers.first } return parent.controllers[index + 1 ] } } }
步骤4
将coordinator
添加为UIPageViewController
的数据源。
PageViewController.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import SwiftUIimport UIKitstruct PageViewController : UIViewControllerRepresentable { var controllers: [UIViewController ] func makeCoordinator () -> Coordinator { Coordinator (self ) } func makeUIViewController (context : Context ) -> UIPageViewController { let pageViewController = UIPageViewController ( transitionStyle: .scroll, navigationOrientation: .horizontal) pageViewController.dataSource = context.coordinator return pageViewController } func updateUIViewController (_ pageViewController : UIPageViewController , context : Context ) { pageViewController.setViewControllers( [controllers[0 ]], direction: .forward, animated: true ) } class Coordinator : NSObject , UIPageViewControllerDataSource { var parent: PageViewController init (_ pageViewController : PageViewController ) { self .parent = pageViewController } func pageViewController ( _ pageViewController : UIPageViewController , viewControllerBefore viewController : UIViewController ) -> UIViewController ? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index == 0 { return parent.controllers.last } return parent.controllers[index - 1 ] } func pageViewController ( _ pageViewController : UIPageViewController , viewControllerAfter viewController : UIViewController ) -> UIViewController ? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index + 1 == parent.controllers.count { return parent.controllers.first } return parent.controllers[index + 1 ] } } }
步骤5
打开实时预览并测试滑动交互。
第三节 在SwiftUI视图状态下跟踪页面
要准备添加自定义UIPageControl
,需要一种方法从PageView
中跟踪当前页。
为此,您将在PageView
中声明@State
属性,并将对该属性的绑定传递到PageViewController
视图。PageViewController
更新绑定以匹配当前可见页。
步骤1
首先添加一个currentPage
绑定作为PageViewController
的属性。
除了声明@Binding
属性外,还要更新对setViewControllers(u:direction:animated:)
的调用,传递currentPage
绑定的值。
PageViewController.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import SwiftUIimport UIKitstruct PageViewController : UIViewControllerRepresentable { var controllers: [UIViewController ] @Binding var currentPage: Int func makeCoordinator () -> Coordinator { Coordinator (self ) } func makeUIViewController (context : Context ) -> UIPageViewController { let pageViewController = UIPageViewController ( transitionStyle: .scroll, navigationOrientation: .horizontal) pageViewController.dataSource = context.coordinator return pageViewController } func updateUIViewController (_ pageViewController : UIPageViewController , context : Context ) { pageViewController.setViewControllers( [controllers[currentPage]], direction: .forward, animated: true ) } class Coordinator : NSObject , UIPageViewControllerDataSource { var parent: PageViewController init (_ pageViewController : PageViewController ) { self .parent = pageViewController } func pageViewController ( _ pageViewController : UIPageViewController , viewControllerBefore viewController : UIViewController ) -> UIViewController ? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index == 0 { return parent.controllers.last } return parent.controllers[index - 1 ] } func pageViewController ( _ pageViewController : UIPageViewController , viewControllerAfter viewController : UIViewController ) -> UIViewController ? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index + 1 == parent.controllers.count { return parent.controllers.first } return parent.controllers[index + 1 ] } } }
步骤2
在PageView
中声明@State
变量,并在创建子PageViewController
时向属性传递绑定。
重要
请记住使用$
语法创建用状态来存储值的绑定。
PageView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import SwiftUIstruct PageView <Page : View >: View { var viewControllers: [UIHostingController <Page >] @State var currentPage = 0 init (_ views : [Page ]) { self .viewControllers = views.map { UIHostingController (rootView: $0 ) } } var body: some View { PageViewController (controllers: viewControllers, currentPage: $currentPage ) } } struct PageView_Preview : PreviewProvider { static var previews: some View { PageView (features.map { FeatureCard (landmark: $0 ) }) .aspectRatio(3 / 2 , contentMode: .fit) } }
步骤3
通过更改PageViewController
的初始值,测试该值是否通过绑定流向PageViewController
。
PageView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import SwiftUIstruct PageView <Page : View >: View { var viewControllers: [UIHostingController <Page >] @State var currentPage = 1 init (_ views : [Page ]) { self .viewControllers = views.map { UIHostingController (rootView: $0 ) } } var body: some View { PageViewController (controllers: viewControllers, currentPage: $currentPage ) } } struct PageView_Preview : PreviewProvider { static var previews: some View { PageView (features.map { FeatureCard (landmark: $0 ) }) .aspectRatio(3 / 2 , contentMode: .fit) } }
实验 :向PageView
添加一个按钮,使PageViewController
跳转到第二个视图。
PageView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import SwiftUIstruct PageView <Page : View >: View { var viewControllers: [UIHostingController <Page >] @State var currentPage = 1 init (_ views : [Page ]) { self .viewControllers = views.map { UIHostingController (rootView: $0 ) } } var body: some View { PageViewController (controllers: viewControllers, currentPage: $currentPage ) } } struct PageView_Preview : PreviewProvider { static var previews: some View { PageView (features.map { FeatureCard (landmark: $0 ) }) .aspectRatio(3 / 2 , contentMode: .fit) } }
步骤4
添加一个带有currentPage
属性的文本视图,这样您就可以监视@State
属性的值。
请注意,当您从一页刷到另一页时,值不会改变。
PageView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import SwiftUIstruct PageView <Page : View >: View { var viewControllers: [UIHostingController <Page >] @State var currentPage = 0 init (_ views : [Page ]) { self .viewControllers = views.map { UIHostingController (rootView: $0 ) } } var body: some View { VStack { PageViewController (controllers: viewControllers, currentPage: $currentPage ) Text ("Current Page: \(currentPage) " ) } } } struct PageView_Preview : PreviewProvider { static var previews: some View { PageView (features.map { FeatureCard (landmark: $0 ) }) } }
步骤5
在PageViewController.swift
中,将协调器设置为UIPageViewControllerDelegate
,并添加pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted completed: Bool)
方法。
由于SwiftUI在页面切换动画完成时调用此方法,因此可以找到当前视图控制器的索引并更新绑定。
PageViewController.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import SwiftUIimport UIKitstruct PageViewController : UIViewControllerRepresentable { var controllers: [UIViewController ] @Binding var currentPage: Int func makeCoordinator () -> Coordinator { Coordinator (self ) } func makeUIViewController (context : Context ) -> UIPageViewController { let pageViewController = UIPageViewController ( transitionStyle: .scroll, navigationOrientation: .horizontal) pageViewController.dataSource = context.coordinator return pageViewController } func updateUIViewController (_ pageViewController : UIPageViewController , context : Context ) { pageViewController.setViewControllers( [controllers[currentPage]], direction: .forward, animated: true ) } class Coordinator : NSObject , UIPageViewControllerDataSource , UIPageViewControllerDelegate { var parent: PageViewController init (_ pageViewController : PageViewController ) { self .parent = pageViewController } func pageViewController ( _ pageViewController : UIPageViewController , viewControllerBefore viewController : UIViewController ) -> UIViewController ? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index == 0 { return parent.controllers.last } return parent.controllers[index - 1 ] } func pageViewController ( _ pageViewController : UIPageViewController , viewControllerAfter viewController : UIViewController ) -> UIViewController ? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index + 1 == parent.controllers.count { return parent.controllers.first } return parent.controllers[index + 1 ] } func pageViewController (_ pageViewController : UIPageViewController , didFinishAnimating finished : Bool , previousViewControllers : [UIViewController ], transitionCompleted completed : Bool ) { if completed, let visibleViewController = pageViewController.viewControllers? .first, let index = parent.controllers.firstIndex(of: visibleViewController) { parent.currentPage = index } } } }
步骤6
除了dataSource
之外,还将coordinator
指定为UIPageViewController
的委托。
当在两个方向上连接绑定后,文本视图会在每次刷新后更新以显示正确的页码。
PageViewController.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import SwiftUIimport UIKitstruct PageViewController : UIViewControllerRepresentable { var controllers: [UIViewController ] @Binding var currentPage: Int func makeCoordinator () -> Coordinator { Coordinator (self ) } func makeUIViewController (context : Context ) -> UIPageViewController { let pageViewController = UIPageViewController ( transitionStyle: .scroll, navigationOrientation: .horizontal) pageViewController.dataSource = context.coordinator pageViewController.delegate = context.coordinator return pageViewController } func updateUIViewController (_ pageViewController : UIPageViewController , context : Context ) { pageViewController.setViewControllers( [controllers[currentPage]], direction: .forward, animated: true ) } class Coordinator : NSObject , UIPageViewControllerDataSource , UIPageViewControllerDelegate { var parent: PageViewController init (_ pageViewController : PageViewController ) { self .parent = pageViewController } func pageViewController ( _ pageViewController : UIPageViewController , viewControllerBefore viewController : UIViewController ) -> UIViewController ? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index == 0 { return parent.controllers.last } return parent.controllers[index - 1 ] } func pageViewController ( _ pageViewController : UIPageViewController , viewControllerAfter viewController : UIViewController ) -> UIViewController ? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index + 1 == parent.controllers.count { return parent.controllers.first } return parent.controllers[index + 1 ] } func pageViewController (_ pageViewController : UIPageViewController , didFinishAnimating finished : Bool , previousViewControllers : [UIViewController ], transitionCompleted completed : Bool ) { if completed, let visibleViewController = pageViewController.viewControllers? .first, let index = parent.controllers.firstIndex(of: visibleViewController) { parent.currentPage = index } } } }
第四节 添加自定义分页控件
您已经准备好向视图中添加自定义UIPageControl
,该控件包装在SwiftUI
的UIViewRepresentable
视图中。
步骤1
创建一个新的SwiftUI视图文件,名为PageControl.swift
。更新PageControl
类型以遵守UIViewRepresentable
协议。
UIViewRepresentable
和UIViewControllerRepresentable
类型具有相同的生命周期,其方法与其基础的UIKit类型相对应。
PageControl.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import SwiftUIimport UIKitstruct PageControl : UIViewRepresentable { var numberOfPages: Int @Binding var currentPage: Int func makeUIView (context : Context ) -> UIPageControl { let control = UIPageControl () control.numberOfPages = numberOfPages return control } func updateUIView (_ uiView : UIPageControl , context : Context ) { uiView.currentPage = currentPage } }
步骤2
将文本框替换为PageControl
,从VStack
切换到ZStack
进行布局。
因为我们正在讲页数和绑定传递到当前页,所以PageControl
已显示正确的值。
PageView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import SwiftUIstruct PageView <Page : View >: View { var viewControllers: [UIHostingController <Page >] @State var currentPage = 0 init (_ views : [Page ]) { self .viewControllers = views.map { UIHostingController (rootView: $0 ) } } var body: some View { ZStack (alignment: .bottomTrailing) { PageViewController (controllers: viewControllers, currentPage: $currentPage ) PageControl (numberOfPages: viewControllers.count, currentPage: $currentPage ) .padding(.trailing) } } } struct PageView_Preview : PreviewProvider { static var previews: some View { PageView (features.map { FeatureCard (landmark: $0 ) }) } }
下一步,让PageControl
交互,这样用户可以点击一边或另一边时,页面可以随之滚动。
步骤3
在PageControl
中创建嵌套的Coordinator
类型,并添加makeCoordinator()
方法以创建并返回新的协调器。
由于UIPageControl
是UIControl
子类,故使用target-action
模式而不是代理,所以此Coordinator
实现@objc
方法来更新 current page
的绑定。
PageControl.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import SwiftUIimport UIKitstruct PageControl : UIViewRepresentable { var numberOfPages: Int @Binding var currentPage: Int func makeCoordinator () -> Coordinator { Coordinator (self ) } func makeUIView (context : Context ) -> UIPageControl { let control = UIPageControl () control.numberOfPages = numberOfPages return control } func updateUIView (_ uiView : UIPageControl , context : Context ) { uiView.currentPage = currentPage } class Coordinator : NSObject { var control: PageControl init (_ control : PageControl ) { self .control = control } @objc func updateCurrentPage (sender : UIPageControl ) { control.currentPage = sender.currentPage } } }
步骤4
将Coordinator
添加为valueChanged
事件的目标,指定updateCurrentPage(sender:)
方法作为要执行的操作。
PageControl.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import SwiftUIimport UIKitstruct PageControl : UIViewRepresentable { var numberOfPages: Int @Binding var currentPage: Int func makeCoordinator () -> Coordinator { Coordinator (self ) } func makeUIView (context : Context ) -> UIPageControl { let control = UIPageControl () control.numberOfPages = numberOfPages control.addTarget( context.coordinator, action: #selector (Coordinator .updateCurrentPage(sender:)), for: .valueChanged) return control } func updateUIView (_ uiView : UIPageControl , context : Context ) { uiView.currentPage = currentPage } class Coordinator : NSObject { var control: PageControl init (_ control : PageControl ) { self .control = control } @objc func updateCurrentPage (sender : UIPageControl ) { control.currentPage = sender.currentPage } } }
步骤5
现在尝试所有不同的交互,PageView
展示了UIKit和SwiftUI视图以及控制器是如何协同工作的。
绘图和动画
视图动画和转场
使用SwiftUI时,无论效果在哪里,都可以对视图或视图状态的更改单独设置动画。SwiftUI负责处理这些组合、重叠和可中断动画的所有复杂性。
在本教程中,您将设置一个视图的动画,该视图包含一个图形,用于跟踪用户在使用Landmarks应用程序时的行为数据。使用animation(_:)
修改器,您将看到设置视图动画是多么容易。
学习时间:25分钟
下载地址:AnimatingViewsAndTransitions.zip
第一节 向各个视图添加动画
在视图上使用animation(_:)
修改器时,SwiftUI可以改变视图的任何animatable(可动画)
属性。视图的颜色color
、不透明度opacity
、旋转rotation
、大小size
和其他属性都是可设置动画的。
步骤1
在HikeView.swift
中,打开实时预览并尝试显示和隐藏图形。
请确保在本教程中使用实时预览,以便您可以尝试每个步骤的结果。
步骤2
通过添加animation(.easeInOut())
为按钮启用旋转动画。
HikeView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import SwiftUIstruct HikeView : View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph (data: hike.observations, path: \.elevation) .frame(width: 50 , height: 30 ) VStack (alignment: .leading) { Text (hike.name) .font(.headline) Text (hike.distanceText) } Spacer () Button (action: { self .showDetail.toggle() }) { Image (systemName: "chevron.right.circle" ) .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0 )) .padding() .animation(.easeInOut()) } } if showDetail { HikeDetail (hike: hike) } } } }
步骤3
当图形可见时,添加一个让按钮变大的动画。
animation(_:)
修饰符将应用于其包装的视图中所有可动画的更改。
HikeView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import SwiftUIstruct HikeView : View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph (data: hike.observations, path: \.elevation) .frame(width: 50 , height: 30 ) VStack (alignment: .leading) { Text (hike.name) .font(.headline) Text (hike.distanceText) } Spacer () Button (action: { self .showDetail.toggle() }) { Image (systemName: "chevron.right.circle" ) .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0 )) .scaleEffect(showDetail ? 1.5 : 1 ) .padding() .animation(.easeInOut()) } } if showDetail { HikeDetail (hike: hike) } } } }
步骤4
将动画类型从easeInOut()
更改为spring()
。
SwiftUI包括预定义或自定义宽松的基本动画,以及弹簧和流体动画。可以调整动画的速度、在动画开始之前设置延迟或指定动画重复。
HikeView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import SwiftUIstruct HikeView : View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph (data: hike.observations, path: \.elevation) .frame(width: 50 , height: 30 ) VStack (alignment: .leading) { Text (hike.name) .font(.headline) Text (hike.distanceText) } Spacer () Button (action: { self .showDetail.toggle() }) { Image (systemName: "chevron.right.circle" ) .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0 )) .scaleEffect(showDetail ? 1.5 : 1 ) .padding() .animation(.spring()) } } if showDetail { HikeDetail (hike: hike) } } } }
步骤5
尝试通过在scaleEffect
修改器的正上方添加另一个动画修改器来关闭旋转的动画。
实验
围绕SwiftUI尝试组合不同的动画效果,看看有什么不同的效果。
HikeView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import SwiftUIstruct HikeView : View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph (data: hike.observations, path: \.elevation) .frame(width: 50 , height: 30 ) VStack (alignment: .leading) { Text (hike.name) .font(.headline) Text (hike.distanceText) } Spacer () Button (action: { self .showDetail.toggle() }) { Image (systemName: "chevron.right.circle" ) .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0 )) .animation(nil ) .scaleEffect(showDetail ? 1.5 : 1 ) .padding() .animation(.spring()) } } if showDetail { HikeDetail (hike: hike) } } } }
步骤6
在转到下一节之前,请删除两个动画(:)修改器。
HikeView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import SwiftUIstruct HikeView : View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph (data: hike.observations, path: \.elevation) .frame(width: 50 , height: 30 ) VStack (alignment: .leading) { Text (hike.name) .font(.headline) Text (hike.distanceText) } Spacer () Button (action: { self .showDetail.toggle() }) { Image (systemName: "chevron.right.circle" ) .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0 )) .scaleEffect(showDetail ? 1.5 : 1 ) .padding() } } if showDetail { HikeDetail (hike: hike) } } } }
第二节 使状态的改变产生动画效果
既然您已经学习了如何将动画应用于各个视图,现在是时候在更改状态值的位置添加动画了。
在这里,您将对用户点击按钮并切换showDetail
属性时发生的所有更改添加动画。
步骤1
用withAnimation
函数包装showDetail.toggle()
的调用。
受showDetail
属性影响的两个视图(disclosure
按钮和HikeDetail
视图)现在都具有动画过渡。
HikeView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import SwiftUIstruct HikeView : View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph (data: hike.observations, path: \.elevation) .frame(width: 50 , height: 30 ) VStack (alignment: .leading) { Text (hike.name) .font(.headline) Text (hike.distanceText) } Spacer () Button (action: { withAnimation { self .showDetail.toggle() } }) { Image (systemName: "chevron.right.circle" ) .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0 )) .scaleEffect(showDetail ? 1.5 : 1 ) .padding() } } if showDetail { HikeDetail (hike: hike) } } } }
减慢动画的速度,以查看SwiftUI动画是如何中断的。
步骤2
将4秒长的基本动画传递给withAnimation
函数。
可以传递相同类型的动画给给animation(:)
修饰符的withAnimation
函数。
HikeView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import SwiftUIstruct HikeView : View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph (data: hike.observations, path: \.elevation) .frame(width: 50 , height: 30 ) VStack (alignment: .leading) { Text (hike.name) .font(.headline) Text (hike.distanceText) } Spacer () Button (action: { withAnimation(.easeInOut(duration: 4 )) { self .showDetail.toggle() } }) { Image (systemName: "chevron.right.circle" ) .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0 )) .scaleEffect(showDetail ? 1.5 : 1 ) .padding() } } if showDetail { HikeDetail (hike: hike) } } } }
步骤3
尝试在动画中打开和关闭图形视图。
步骤4
在继续下一节之前,请从withAnimation
函数调用中删除慢速动画。
HikeView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import SwiftUIstruct HikeView : View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph (data: hike.observations, path: \.elevation) .frame(width: 50 , height: 30 ) VStack (alignment: .leading) { Text (hike.name) .font(.headline) Text (hike.distanceText) } Spacer () Button (action: { withAnimation { self .showDetail.toggle() } }) { Image (systemName: "chevron.right.circle" ) .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0 )) .scaleEffect(showDetail ? 1.5 : 1 ) .padding() } } if showDetail { HikeDetail (hike: hike) } } } }
第三节 自定义视图转场
默认情况下,视图通过淡入淡出在屏幕内外切换。您可以使用transition(:)
修饰符自定义此转场。
步骤1
向条件可见的HikeView
添加一个transition(_:)
修饰符。
现在,图形通过滑动进入和退出视线而出现和消失。
HikeView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import SwiftUIstruct HikeView : View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph (data: hike.observations, path: \.elevation) .frame(width: 50 , height: 30 ) VStack (alignment: .leading) { Text (hike.name) .font(.headline) Text (hike.distanceText) } Spacer () Button (action: { withAnimation { self .showDetail.toggle() } }) { Image (systemName: "chevron.right.circle" ) .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0 )) .scaleEffect(showDetail ? 1.5 : 1 ) .padding() } } if showDetail { HikeDetail (hike: hike) .transition(.slide) } } } }
步骤2
将转场提取为AnyTransition
的静态属性。
这将在展开自定义转场时保持代码的干净。对于自定义转换,您可以使用与SwiftUI相同的.
语法。
HikeView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import SwiftUIextension AnyTransition { static var moveAndFade: AnyTransition { AnyTransition .slide } } struct HikeView : View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph (data: hike.observations, path: \.elevation) .frame(width: 50 , height: 30 ) VStack (alignment: .leading) { Text (hike.name) .font(.headline) Text (hike.distanceText) } Spacer () Button (action: { withAnimation { self .showDetail.toggle() } }) { Image (systemName: "chevron.right.circle" ) .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0 )) .scaleEffect(showDetail ? 1.5 : 1 ) .padding() } } if showDetail { HikeDetail (hike: hike) .transition(.moveAndFade) } } } }
步骤3
切换到使用move(edge:)
转场,以便图形从同一侧滑入和滑出。
HikeView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import SwiftUIextension AnyTransition { static var moveAndFade: AnyTransition { AnyTransition .move(edge: .trailing) } } struct HikeView : View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph (data: hike.observations, path: \.elevation) .frame(width: 50 , height: 30 ) VStack (alignment: .leading) { Text (hike.name) .font(.headline) Text (hike.distanceText) } Spacer () Button (action: { withAnimation { self .showDetail.toggle() } }) { Image (systemName: "chevron.right.circle" ) .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0 )) .scaleEffect(showDetail ? 1.5 : 1 ) .padding() } } if showDetail { HikeDetail (hike: hike) .transition(.moveAndFade) } } } }
步骤4
使用asymmetric(insertion:removal:)
(不对称)修饰符在视图出现和消失时提供不同的过渡。
HikeView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import SwiftUIextension AnyTransition { static var moveAndFade: AnyTransition { let insertion = AnyTransition .move(edge: .trailing) .combined(with: .opacity) let removal = AnyTransition .scale .combined(with: .opacity) return .asymmetric(insertion: insertion, removal: removal) } } struct HikeView : View { var hike: Hike @State private var showDetail = false var body: some View { VStack { HStack { HikeGraph (data: hike.observations, path: \.elevation) .frame(width: 50 , height: 30 ) VStack (alignment: .leading) { Text (hike.name) .font(.headline) Text (hike.distanceText) } Spacer () Button (action: { withAnimation { self .showDetail.toggle() } }) { Image (systemName: "chevron.right.circle" ) .imageScale(.large) .rotationEffect(.degrees(showDetail ? 90 : 0 )) .scaleEffect(showDetail ? 1.5 : 1 ) .padding() } } if showDetail { HikeDetail (hike: hike) .transition(.moveAndFade) } } } }
第四节 为复杂效果撰写动画
单击栏下方的按钮时,图形将在三组不同的数据之间切换。在本节中,将使用组合动画为构成图形的胶囊提供动态、波动的转场动画。
步骤1
将showDetail
的默认值更改为true,并将HikeView
预览锁定到画布。
这使您在另一个文件中处理动画时依然可以在上下文中看到图表。
步骤2
在GraphCapsule.swift
中,添加新的计算动画属性,并将其应用于胶囊形状。
GraphCapsule.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import SwiftUIstruct GraphCapsule : View { var index: Int var height: CGFloat var range: Range <Double > var overallRange: Range <Double > var heightRatio: CGFloat { max (CGFloat (magnitude(of: range) / magnitude(of: overallRange)), 0.15 ) } var offsetRatio: CGFloat { CGFloat ((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange)) } var animation: Animation { Animation .default } var body: some View { Capsule () .fill(Color .gray) .frame(height: height * heightRatio, alignment: .bottom) .offset(x: 0 , y: height * - offsetRatio) .animation(animation) ) } }
步骤3
将动画切换为弹簧动画spring
,减少阻尼部分dampingFraction
以使条形可以跳跃。
GraphCapsule.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import SwiftUIstruct GraphCapsule : View { var index: Int var height: CGFloat var range: Range <Double > var overallRange: Range <Double > var heightRatio: CGFloat { max (CGFloat (magnitude(of: range) / magnitude(of: overallRange)), 0.15 ) } var offsetRatio: CGFloat { CGFloat ((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange)) } var animation: Animation { Animation .spring(dampingFraction: 0.5 ) } var body: some View { Capsule () .fill(Color .gray) .frame(height: height * heightRatio, alignment: .bottom) .offset(x: 0 , y: height * - offsetRatio) .animation(animation) ) } }
步骤4
稍微加快动画的速度,以缩短每个条形移动到新位置所需的时间。
GraphCapsule.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import SwiftUIstruct GraphCapsule : View { var index: Int var height: CGFloat var range: Range <Double > var overallRange: Range <Double > var heightRatio: CGFloat { max (CGFloat (magnitude(of: range) / magnitude(of: overallRange)), 0.15 ) } var offsetRatio: CGFloat { CGFloat ((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange)) } var animation: Animation { Animation .spring(dampingFraction: 0.5 ) .speed(2 ) } var body: some View { Capsule () .fill(Color .gray) .frame(height: height * heightRatio, alignment: .bottom) .offset(x: 0 , y: height * - offsetRatio) .animation(animation) ) } }
步骤5
根据胶囊在图形上的位置为每个动画添加延迟delay
。
GraphCapsule.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import SwiftUIstruct GraphCapsule : View { var index: Int var height: CGFloat var range: Range <Double > var overallRange: Range <Double > var heightRatio: CGFloat { max (CGFloat (magnitude(of: range) / magnitude(of: overallRange)), 0.15 ) } var offsetRatio: CGFloat { CGFloat ((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange)) } var animation: Animation { Animation .spring(dampingFraction: 0.5 ) .speed(2 ) .delay(0.03 * Double (index)) } var body: some View { Capsule () .fill(Color .gray) .frame(height: height * heightRatio, alignment: .bottom) .offset(x: 0 , y: height * - offsetRatio) .animation(animation) ) } }
步骤6
观察自定义动画在图形之间转换时如何提供波动效果。
构建列表和导航
设置了基本地标详情视图后,需要为用户提供查看地标所有列表和查看每个地标详情的方法。
您将创建显示所有地标信息的视图,并动态生成一个滚动列表,用户可以点击该列表查看地标的详情视图。要微调UI,您将使用Xcode的画布以不同的设备大小呈现多个预览。
下载项目文件以开始构建此项目,并执行以下步骤。
学习时间:35分钟
下载示例:BuildingListsAndNavigation.zip
第一节 了解示例数据。
在第一个教程中,我们将数据硬编码到所有自定义视图中。在这里,您将学习如何将数据传递到自定义视图中以动态显示。
首先下载示例项目 并熟悉示例数据。
步骤1
在项目导航器中,选择Models>Landmark.swift
。
Landmark.swift
声明了一个Landmark结构,该结构存储应用程序需要显示的所有Landmark
信息,并从landmarkData.json
导入一组Landmark
数据。
Landmark.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import SwiftUIimport CoreLocationstruct Landmark : Hashable , Codable { var id: Int var name: String fileprivate var imageName: String fileprivate var coordinates: Coordinates var state: String var park: String var category: Category var locationCoordinate: CLLocationCoordinate2D { CLLocationCoordinate2D ( latitude: coordinates.latitude, longitude: coordinates.longitude) } enum Category : String , CaseIterable , Codable , Hashable { case featured = "Featured" case lakes = "Lakes" case rivers = "Rivers" } } extension Landmark { var image: Image { ImageStore .shared.image(name: imageName) } } struct Coordinates : Hashable , Codable { var latitude: Double var longitude: Double }
步骤2
在项目导航器中,选择Resources>landmarkData.json
。
您将在本教程的其余部分以及随后的所有内容中使用此示例数据。
landmarkData.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 [ { "name": "Turtle Rock", "category": "Featured", "city": "Twentynine Palms", "state": "California", "id": 1001, "park": "Joshua Tree National Park", "coordinates": { "longitude": -116.166868, "latitude": 34.011286 }, "imageName": "turtlerock" }, { "name": "Silver Salmon Creek", "category": "Lakes", "city": "Port Alsworth", "state": "Alaska", "id": 1002, "park": "Lake Clark National Park and Preserve", "coordinates": { "longitude": -152.665167, "latitude": 59.980167 }, "imageName": "silversalmoncreek" }, ... ]
步骤3
请注意,我们将ContentView
类型重命名为LandmarkDetail
。
在本教程和以下每个教程中,您将创建多个视图类型。
LandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import SwiftUIstruct LandmarkDetail : View { var body: some View { VStack { MapView () .frame(height: 300 ) CircleImage () .offset(y: - 130 ) .padding(.bottom, - 130 ) VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) HStack (alignment: .top) { Text ("Joshua Tree National Park" ) .font(.subheadline) Spacer () Text ("California" ) .font(.subheadline) } } .padding() Spacer () } } }
第二节 创建行视图
本节中构建的第一个视图是行视图,用于显示每个地标的详细信息。此行视图声明了一个属性即landmark
,其用于存储地标信息,以便行视图可以显示任何地标。稍后,您将把多个行视图合并成一个地标列表。
步骤1
创建一个新的SwiftUI视图,名为LandmarkRow.swift
。
步骤2
如果预览尚未显示,请通过选择Editor > Editor and Canvas
,来显示画布,然后单击“Resume”。
步骤3
添加landmark
作为LandmarkRow
的存储属性。
添加landmark
属性时,预览将停止工作,因为LandmarkRow
类型在初始化期间需要landmark
实例。
LandmarkRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import SwiftUIstruct LandmarkRow : View { var landmark: Landmark var body: some View { Text ("Hello World" ) } } struct LandmarkRow_Previews : PreviewProvider { static var previews: some View { LandmarkRow () } }
想要修复预览,需要修改PreviewProvider
。
步骤4
在LandmarkRow_Previews
的static previews
属性中,将landmarkData
数组的第一个元素作为LandmarkRow
参数添加到LandmarkRow初始值设定项中。
预览可以正常显示文本Hello World
了。
LandmarkRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import SwiftUIstruct LandmarkRow : View { var landmark: Landmark var body: some View { Text ("Hello World" ) } } struct LandmarkRow_Previews : PreviewProvider { static var previews: some View { LandmarkRow (landmark: landmarkData[0 ]) } }
修复后,可以为行视图生成布局。
步骤5
将文本视图嵌入到HStack
中。
LandmarkRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import SwiftUIstruct LandmarkRow : View { var landmark: Landmark var body: some View { HStack { Text ("Hello World" ) } } } struct LandmarkRow_Previews : PreviewProvider { static var previews: some View { LandmarkRow (landmark: landmarkData[0 ]) } }
步骤6
修改文本视图以使用landmark
的name
属性。
LandmarkRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import SwiftUIstruct LandmarkRow : View { var landmark: Landmark var body: some View { HStack { Text (landmark.name) } } } struct LandmarkRow_Previews : PreviewProvider { static var previews: some View { LandmarkRow (landmark: landmarkData[0 ]) } }
步骤7
在Text
视图之前添加Image
视图来完成行视图。
LandmarkRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import SwiftUIstruct LandmarkRow : View { var landmark: Landmark var body: some View { HStack { landmark.image .resizable() .frame(width: 50 , height: 50 ) Text (landmark.name) } } } struct LandmarkRow_Previews : PreviewProvider { static var previews: some View { LandmarkRow (landmark: landmarkData[0 ]) } }
第三节 自定义行预览
Xcode的画布自动识别并显示当前编辑器中遵守PreviewProvider
协议的任何类型。PreviewProvider
返回一个或多个视图,并提供配置大小和设备的选项。
您可以自定义PreviewProvider
返回的内容,以准确呈现对您最有帮助的预览。
步骤1
在LandmarkRow_Previews
中,将landmark
参数更新为landmarkData
数组中的第二个元素。
预览立即显示第二个示例地标,而不是第一个。
LandmarkRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import SwiftUIstruct LandmarkRow : View { var landmark: Landmark var body: some View { HStack { landmark.image .resizable() .frame(width: 50 , height: 50 ) Text (landmark.name) } } } struct LandmarkRow_Previews : PreviewProvider { static var previews: some View { LandmarkRow (landmark: landmarkData[1 ]) } }
步骤2
使用previewLayout(_:)
设置一个与列表中的行近似的大小。
LandmarkRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import SwiftUIstruct LandmarkRow : View { var landmark: Landmark var body: some View { HStack { landmark.image .resizable() .frame(width: 50 , height: 50 ) Text (landmark.name) } } } struct LandmarkRow_Previews : PreviewProvider { static var previews: some View { LandmarkRow (landmark: landmarkData[1 ]) .previewLayout(.fixed(width: 300 , height: 70 )) } }
可以使用Group
从PreviewProvider
返回多个视图预览。
步骤3
将返回的行包装在一个Group
中,然后再次添加第一行。
Group
是用于对视图内容进行分组的容器。Xcode将组的子视图呈现为画布中的单独预览。
LandmarkRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import SwiftUIstruct LandmarkRow : View { var landmark: Landmark var body: some View { HStack { landmark.image .resizable() .frame(width: 50 , height: 50 ) Text (landmark.name) } } } struct LandmarkRow_Previews : PreviewProvider { static var previews: some View { Group { LandmarkRow (landmark: landmarkData[0 ]) .previewLayout(.fixed(width: 300 , height: 70 )) LandmarkRow (landmark: landmarkData[1 ]) .previewLayout(.fixed(width: 300 , height: 70 )) } } }
步骤4
要简化代码,请将previewLayout(:)
调用移到Group
的外部。
视图的子级视图继承视图的上下文设置,如预览配置。
LandmarkRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import SwiftUIstruct LandmarkRow : View { var landmark: Landmark var body: some View { HStack { landmark.image .resizable() .frame(width: 50 , height: 50 ) Text (landmark.name) } } } struct LandmarkRow_Previews : PreviewProvider { static var previews: some View { Group { LandmarkRow (landmark: landmarkData[0 ]) LandmarkRow (landmark: landmarkData[1 ]) } .previewLayout(.fixed(width: 300 , height: 70 )) } }
Tips:在PreviewProvider
中编写的代码只会改变Xcode在画布中显示的内容。
第四节 创建地标列表
使用SwiftUI的List
类型时,可以显示特定于平台的视图列表。列表的元素可以是静态的,比如您目前创建的堆栈的子视图,也可以是动态生成的。您甚至可以混合静态和动态生成的视图。
步骤1
创建一个新的SwiftUI View
,名为LandmarkList.swift
。
步骤2
将默认Text
视图替换为List
,并提供前两个Landmark
作为列表子级的LandmarkRow
实例。
预览显示了两个地标,它们以适合iOS的列表样式呈现。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import SwiftUIstruct LandmarkList : View { var body: some View { List { LandmarkRow (landmark: landmarkData[0 ]) LandmarkRow (landmark: landmarkData[1 ]) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () } }
第五节 动态化列表
您可以直接从集合中生成行,而不是单独的指定列表中的元素。
通过传递数据集合并为集合中的每个元素提供视图的闭包,可以创建显示集合元素的列表。列表使用提供的闭包将集合中的每个元素转换为子视图。
步骤1
删除两个静态LandmarkRow
,并将landmarkData
传递给Lisst
初始值设定项。
List
使用identifiable
数据。您可以通过以下两种方式之一生产identifiable
数据:1、使用key path
属性标识每个元素;2、使数据遵守Identifiable
协议。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import SwiftUIstruct LandmarkList : View { var body: some View { List (landmarkData, id: \.id) { landmark in } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () } }
步骤2
通过从闭包返回LandmarkRow
来动态生成列表。
这将为landmarkData
数组中的每个元素创建一个LandmarkRow
。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import SwiftUIstruct LandmarkList : View { var body: some View { List (landmarkData, id: \.id) { landmark in LandmarkRow (landmark: landmark) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () } }
接下来,您将通过使Landmark
遵守Identifiable
来简化代码。
步骤3
切换到Landmark.swift
并声明Identifiable
协议。
由于Landmark
类型已经具有Identifiable
所需的id属性,因此没有其他工作要做。
Landmark.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import SwiftUIimport CoreLocationstruct Landmark : Hashable , Codable , Identifiable { var id: Int var name: String fileprivate var imageName: String fileprivate var coordinates: Coordinates var state: String var park: String var category: Category var locationCoordinate: CLLocationCoordinate2D { CLLocationCoordinate2D ( latitude: coordinates.latitude, longitude: coordinates.longitude) } enum Category : String , CaseIterable , Codable , Hashable { case featured = "Featured" case lakes = "Lakes" case rivers = "Rivers" } } extension Landmark { var image: Image { ImageStore .shared.image(name: imageName) } } struct Coordinates : Hashable , Codable { var latitude: Double var longitude: Double }
步骤4
切换回LandmarkList.swift
并删除id参数。
从现在起,您将能够直接使用Landmark
的集合。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import SwiftUIstruct LandmarkList : View { var body: some View { List (landmarkData) { landmark in LandmarkRow (landmark: landmark) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () } }
第六节 在列表和详情之间设置导航
列表呈现正确,但您还不能轻触每个LandmarkRow
来查看该地标的详情页面。
通过把List
嵌入到NavigationView
中使其具有导航功能,然后在NavigationLink
中嵌入每一行LandmarkRow
,并设置要跳转的目标视图。
步骤1
在NavigationView
中嵌入动态生成的LandmarkList
。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import SwiftUIstruct LandmarkList : View { var body: some View { NavigationView { List (landmarkData) { landmark in LandmarkRow (landmark: landmark) } } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () } }
步骤2
在显示列表时,调用navigationBarTitle(:)
修饰符方法设置导航栏的标题。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import SwiftUIstruct LandmarkList : View { var body: some View { NavigationView { List (landmarkData) { landmark in LandmarkRow (landmark: landmark) } .navigationBarTitle(Text ("Landmarks" )) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () } }
步骤3
在List
的闭包中,将返回的Row
包装在NavigationLink
中,指定LandmarkDetail
视图作为跳转目标。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import SwiftUIstruct LandmarkList : View { var body: some View { NavigationView { List (landmarkData) { landmark in NavigationLink (destination: LandmarkDetail ()) { LandmarkRow (landmark: landmark) } } .navigationBarTitle(Text ("Landmarks" )) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () } }
步骤4
通过切换到实时预览模式,您可以在预览中直接尝试导航功能。单击Live Preview
按钮并点击地标行访问详情页。
第7节 将数据传递到子视图
LandmarkDetail
视图仍然使用硬编码方式来显示地标。与LandmarkRow
一样,LandmarkDetail
类型及其包含的视图需要使用landmark
属性作为其数据源。
从子视图开始,您将转换CircleImage
、MapView
和LandmarkDetail
以显示传入的数据,而不是对每行进行硬编码。
步骤1
在CircleImage.swift
中,将存储属性image
添加到CircleImage
。
这是使用SwiftUI构建视图时的常见模式。您的自定义视图通常会包装一系列修饰符。
CircleImage.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import SwiftUIstruct CircleImage : View { var image: Image var body: some View { image .clipShape(Circle ()) .overlay(Circle ().stroke(Color .white, lineWidth: 4 )) .shadow(radius: 10 ) } } struct CircleImage_Preview : PreviewProvider { static var previews: some View { CircleImage () } }
步骤2
更新CircleImage_Preview
以传递名为Turtle Rock
的Image
。
CircleImage.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import SwiftUIstruct CircleImage : View { var image: Image var body: some View { image .clipShape(Circle ()) .overlay(Circle ().stroke(Color .white, lineWidth: 4 )) .shadow(radius: 10 ) } } struct CircleImage_Preview : PreviewProvider { static var previews: some View { CircleImage (image: Image ("turtlerock" )) } }
步骤3
在MapView.swift
中,向MapView
添加一个coordinate
属性,并将代码转换为使用该属性,而不是硬编码纬度和经度。
MapView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import SwiftUIimport MapKitstruct MapView : UIViewRepresentable { var coordinate: CLLocationCoordinate2D func makeUIView (context : Context ) -> MKMapView { MKMapView (frame: .zero) } func updateUIView (_ view : MKMapView , context : Context ) { let span = MKCoordinateSpan (latitudeDelta: 0.02 , longitudeDelta: 0.02 ) let region = MKCoordinateRegion (center: coordinate, span: span) view.setRegion(region, animated: true ) } } struct MapView_Preview : PreviewProvider { static var previews: some View { MapView () } }
步骤4
更新MapView_Preview
以传递数据数组中第一个地标元素的坐标。
MapView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import SwiftUIimport MapKitstruct MapView : UIViewRepresentable { var coordinate: CLLocationCoordinate2D func makeUIView (context : Context ) -> MKMapView { MKMapView (frame: .zero) } func updateUIView (_ view : MKMapView , context : Context ) { let span = MKCoordinateSpan (latitudeDelta: 0.02 , longitudeDelta: 0.02 ) let region = MKCoordinateRegion (center: coordinate, span: span) view.setRegion(region, animated: true ) } } struct MapView_Preview : PreviewProvider { static var previews: some View { MapView (coordinate: landmarkData[0 ].locationCoordinate) } }
步骤5
在LandmarkDetail.swift
中,将Landmark
属性添加到LandmarkDetail
类中。
LandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import SwiftUIstruct LandmarkDetail : View { var landmark: Landmark var body: some View { VStack { MapView () .frame(height: 300 ) CircleImage () .offset(y: - 130 ) .padding(.bottom, - 130 ) VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) HStack (alignment: .top) { Text ("Joshua Tree National Park" ) .font(.subheadline) Spacer () Text ("California" ) .font(.subheadline) } } .padding() Spacer () } } } struct LandmarkDetail_Preview : PreviewProvider { static var previews: some View { LandmarkDetail () } }
步骤6
更新LandmarkDetail_Preview
以使用landmarkData
数组中的第一个地标元素。
LandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import SwiftUIstruct LandmarkDetail : View { var landmark: Landmark var body: some View { VStack { MapView () .frame(height: 300 ) CircleImage () .offset(y: - 130 ) .padding(.bottom, - 130 ) VStack (alignment: .leading) { Text ("Turtle Rock" ) .font(.title) HStack (alignment: .top) { Text ("Joshua Tree National Park" ) .font(.subheadline) Spacer () Text ("California" ) .font(.subheadline) } } .padding() Spacer () } } } struct LandmarkDetail_Preview : PreviewProvider { static var previews: some View { LandmarkDetail (landmark: landmarkData[0 ]) } }
步骤7
将所需的数据下传给自定义类型。
LandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import SwiftUIstruct LandmarkDetail : View { var landmark: Landmark var body: some View { VStack { MapView (coordinate: landmark.locationCoordinate) .frame(height: 300 ) CircleImage (image: landmark.image) .offset(y: - 130 ) .padding(.bottom, - 130 ) VStack (alignment: .leading) { Text (landmark.name) .font(.title) HStack (alignment: .top) { Text (landmark.park) .font(.subheadline) Spacer () Text (landmark.state) .font(.subheadline) } } .padding() Spacer () } } } struct LandmarkDetail_Preview : PreviewProvider { static var previews: some View { LandmarkDetail (landmark: landmarkData[0 ]) } }
步骤8
最后,调用navigationBarTitle(_:displayMode:)
修饰符,在显示详情视图时为导航栏提供标题。
LandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import SwiftUIstruct LandmarkDetail : View { var landmark: Landmark var body: some View { VStack { MapView (coordinate: landmark.locationCoordinate) .frame(height: 300 ) CircleImage (image: landmark.image) .offset(y: - 130 ) .padding(.bottom, - 130 ) VStack (alignment: .leading) { Text (landmark.name) .font(.title) HStack (alignment: .top) { Text (landmark.park) .font(.subheadline) Spacer () Text (landmark.state) .font(.subheadline) } } .padding() Spacer () } .navigationBarTitle(Text (landmark.name), displayMode: .inline) } } struct LandmarkDetail_Preview : PreviewProvider { static var previews: some View { LandmarkDetail (landmark: landmarkData[0 ]) } }
步骤9
在SceneDelegate.swift
中,将应用程序的根视图切换为LandmarkList
。
当在模拟器中独立运行(不是预览模式)时,您的应用程序将从SceneDelegate
中定义的根视图开始。
SceneDelegate.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import UIKitimport SwiftUIclass SceneDelegate : UIResponder , UIWindowSceneDelegate { var window: UIWindow ? func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene .ConnectionOptions ) { if let windowScene = scene as? UIWindowScene { let window = UIWindow (windowScene: windowScene) window.rootViewController = UIHostingController (rootView: LandmarkList ()) self .window = window window.makeKeyAndVisible() } } }
步骤10
在LandmarkList.swift
中,将当前Landmark
传递到目标详情页。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import SwiftUIstruct LandmarkList : View { var body: some View { NavigationView { List (landmarkData) { landmark in NavigationLink (destination: LandmarkDetail (landmark: landmark)) { LandmarkRow (landmark: landmark) } } .navigationBarTitle(Text ("Landmarks" )) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () } }
步骤11
切换到实时预览以查看从列表导航到详情视图时,是否显示正确的地标。
第8节 动态生成预览
接下来,将向LandmarkList_Previews
添加代码,以呈现不同设备大小的列表视图预览。默认情况下,预览以激活状态设备的大小呈现。可以通过调用previewDevice(:)
修饰符方法来更改预览设备。
步骤1
首先,将当前列表预览更改为iPhone SE
大小的渲染器。
您可以提供Xcode
的scheme
菜单中显示的任何设备的名称。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import SwiftUIstruct LandmarkList : View { var body: some View { NavigationView { List (landmarkData) { landmark in NavigationLink (destination: LandmarkDetail (landmark: landmark)) { LandmarkRow (landmark: landmark) } } .navigationBarTitle(Text ("Landmarks" )) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () .previewDevice(PreviewDevice (rawValue: "iPhone SE" )) } }
步骤2
在LandmarkList_Previews
中,使用设备名称数组作为数据,将LandmarkList
嵌入ForEach
实例中。
ForEach对集合的操作方式与list相同,这意味着您可以在任何可以使用子视图的地方使用它,例如在stacks
、list
、group
等中。当数据元素是简单的值类型(如您在这里使用的字符串)时,可以使用.self作为identifier
的key path
。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import SwiftUIstruct LandmarkList : View { var body: some View { NavigationView { List (landmarkData) { landmark in NavigationLink (destination: LandmarkDetail (landmark: landmark)) { LandmarkRow (landmark: landmark) } } .navigationBarTitle(Text ("Landmarks" )) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { ForEach (["iPhone SE" , "iPhone XS Max" ], id: \.self ) { deviceName in LandmarkList () .previewDevice(PreviewDevice (rawValue: deviceName)) } } }
步骤3
使用previewDisplayName(_:)
修饰符将设备名称添加为预览的标签。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import SwiftUIstruct LandmarkList : View { var body: some View { NavigationView { List (landmarkData) { landmark in NavigationLink (destination: LandmarkDetail (landmark: landmark)) { LandmarkRow (landmark: landmark) } } .navigationBarTitle(Text ("Landmarks" )) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { ForEach (["iPhone SE" , "iPhone XS Max" ], id: \.self ) { deviceName in LandmarkList () .previewDevice(PreviewDevice (rawValue: deviceName)) .previewDisplayName(deviceName) } } }
步骤4
您可以尝试使用不同的设备来比较视图的渲染,所有这些都来自画布。
框架集成
创建watchOS应用程序
本教程为您提供了一个机会,让您可以应用您已经了解的有关SwiftUI的大部分内容,并且只需很少的努力将Landmarks
应用程序迁移到watchOS
。
在复制为iOS应用程序创建的共享数据和视图之前,您将首先向项目中添加watchOS target
。所有资源就绪后,您将自定义SwiftUI视图,以便在watchOS
上显示详情视图和列表视图。
学习时间:25分钟
下载地址:CreatingAwatchOSApp.zip
第一节 添加watchOS Target
要创建watchOS应用程序,请首先将watchOS Target添加到项目中。Xcode将watchOS应用程序的文件夹和文件,以及构建和运行该应用程序所需的方案添加到项目中。
步骤1
选择File > New > Target
。当工作模板表出现时,选择watchOS
选项卡,选择Watch App for iOS
应用程序模板,然后单击Next
。
此模板将新的watchOS应用程序添加到您的项目中,并与iOS应用程序配对。
步骤2
在表单中,输入WatchLandmarks
作为产品名称。将语言设置为Swift
,将用户界面设置为SwiftUI
。选中Include Notification Scene
复选框,然后单击“Finish”。
步骤3
如果Xcode提示,请单击“Activate”。
这将选择WatchLandmarks
方案,以便您可以构建和运行watchOS应用程序。
步骤4
在WatchLandmarks
扩展的“General”选项卡中,选中Supports Running Without iOS App Installation(支持在不安装iOS应用程序的情况下运行)
复选框。
尽可能创建一个独立的watchOS应用程序。独立的watchOS应用不需要iOS配套应用。
第二节 在目标之间共享文件
设置了watchOS目标后,需要共享iOS目标中的一些资源。您将重用Landmark
应用程序的数据模型、一些资源文件以及两个平台都可以显示而无需修改的任何视图。
步骤1
在项目导航器中,单击命令以选择以下文件:LandmarkRow.swift、Landmark.swift、UserData.swift、Data.swift、Profile.swift、Hike.swift和CircleImage.swift。
Landmark.swift、UserData.swift、Data.swift、Profile.swift和Hike.swift定义了应用程序的数据模型。您不会使用模型的所有方面,但需要所有文件才能成功编译应用程序。LandmarkRow.swift和CircleImage.swift都是应用程序可以在watchOS上显示的视图,无需任何更改。
步骤2
在“文件检查器”中,选中“Target Membership”部分中的“WatchLandmarks Extension”复选框。
这使您在上一步中选择的文件可用于watchOS应用程序。
步骤3
在项目导航器中,选择Landmark组中的Assets.xclassets文件,并将其添加到File
检查器的Target Membership
部分中的WatchLandmarks
Target中。
这与您在上一步中选择的Target
不同。WatchLandmarks Extension
Target包含你的应用程序的代码,而WatchLandmarks Target
管理脚本、图标和相关资源。
步骤4
在项目导航器中,选择Resources
文件夹中的所有文件,然后在File
检查器的Target Membership
中将它们添加到WatchLandmarks Extension
target中。
第三节 创建详情视图
现在iOS目标资源已经准备好用于watch应用程序,您需要创建一个watch的视图来显示地标详情。为了测试详情视图,您将为最大和最小的手表尺寸创建自定义预览,并对圆形视图进行一些更改,以便所有内容都适合手表界面。
步骤1
在项目导航器中,单击WatchLandmarks Extension
文件夹旁边的三角形以显示其内容,然后添加名为WatchLandmarkDetail
的新SwiftUI视图。
步骤2
将userData、landmark、landmarkIndex
属性添加到WatchLandmarkDetail
结构中。
这些属性与您在Handling User Input时添加到LandmarkDetail
结构中的属性相同。
WatchLandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import SwiftUIstruct WatchLandmarkDetail : View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0 .id == landmark.id })! } var body: some View { Text ("Hello World!" ) } } struct WatchLandmarkDetail_Previews : PreviewProvider { static var previews: some View { WatchLandmarkDetail () } }
在上一步中添加属性后,Xcode中将出现参数丢失错误。要修复错误,您需要执行以下两项操作之一:为属性提供默认值,或传递参数设置视图的属性。
步骤3
在预览中,创建用户数据的实例,并使用它将landmark
对象传递给WatchLandmarkView
结构的初始值。还需要将用户数据设置为视图的环境对象。
WatchLandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import SwiftUIstruct WatchLandmarkDetail : View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0 .id == landmark.id })! } var body: some View { Text ("Hello World!" ) } } struct WatchLandmarkDetail_Previews : PreviewProvider { static var previews: some View { let userData = UserData () return WatchLandmarkDetail (landmark: userData.landmarks[0 ]) .environmentObject(userData) } }
步骤4
在WatchLandmarkDetail.swift
中,从body()
方法返回CircleImage
视图。
在这里,您可以重用iOS项目中的CircleImage视图。因为您创建了一个可调整大小的图像,所以调用.scaledToFill()
将调整圆的大小,使其自动适配显示。
WatchLandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import SwiftUIstruct WatchLandmarkDetail : View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0 .id == landmark.id })! } var body: some View { CircleImage (image: self .landmark.image.resizable()) .scaledToFill() } } struct WatchLandmarkDetail_Previews : PreviewProvider { static var previews: some View { let userData = UserData () return WatchLandmarkDetail (landmark: userData.landmarks[0 ]) .environmentObject(userData) } }
步骤5
为最大(44毫米)和最小(38毫米)的表盘创建预览。
通过对最大和最小的手表表面进行测试,你可以看到你的应用程序在屏幕上的缩放效果。一如既往,您应该在所有支持的设备大小上测试用户界面。
WatchLandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import SwiftUIstruct WatchLandmarkDetail : View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0 .id == landmark.id })! } var body: some View { CircleImage (image: self .landmark.image.resizable()) .scaledToFill() } } struct WatchLandmarkDetail_Previews : PreviewProvider { static var previews: some View { let userData = UserData () return Group { WatchLandmarkDetail (landmark: userData.landmarks[0 ]).environmentObject(userData) .previewDevice("Apple Watch Series 4 - 44mm" ) WatchLandmarkDetail (landmark: userData.landmarks[1 ]).environmentObject(userData) .previewDevice("Apple Watch Series 2 - 38mm" ) } } }
圆形图像将调整大小以适配显示的高度。不幸的是,这也限制了圆的宽度。要解决裁减问题,您需要将图像嵌入VStack
中,并进行一些额外的布局更改,以便圆形图像适合任何手表的宽度。
步骤6
将圆图像嵌入到VStack
中。在图像下方显示地标名称及其信息。
如您所见,该信息不太适合在表盘屏幕上显示,但您可以通过将VStack放在滚动视图中来解决这个问题。
WatchLandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import SwiftUIstruct WatchLandmarkDetail : View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0 .id == landmark.id })! } var body: some View { VStack { CircleImage (image: self .landmark.image.resizable()) .scaledToFill() Text (self .landmark.name) .font(.headline) .lineLimit(0 ) Toggle (isOn: $userData .landmarks[self .landmarkIndex].isFavorite) { Text ("Favorite" ) } Divider () Text (self .landmark.park) .font(.caption) .bold() .lineLimit(0 ) Text (self .landmark.state) .font(.caption) } } } struct WatchLandmarkDetail_Previews : PreviewProvider { static var previews: some View { let userData = UserData () return Group { WatchLandmarkDetail (landmark: userData.landmarks[0 ]).environmentObject(userData) .previewDevice("Apple Watch Series 4 - 44mm" ) WatchLandmarkDetail (landmark: userData.landmarks[1 ]).environmentObject(userData) .previewDevice("Apple Watch Series 2 - 38mm" ) } } }
步骤7
在滚动视图中包装VStack
。
这会打开视图滚动,但会产生另一个问题:圆形图像现在会扩展到原大小,并且会调整其他UI元素的大小以匹配图像大小。您需要调整圆图像的大小,以便屏幕上只显示圆和地标名称。
WatchLandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import SwiftUIstruct WatchLandmarkDetail : View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0 .id == landmark.id })! } var body: some View { ScrollView { VStack { CircleImage (image: self .landmark.image.resizable()) .scaledToFill() Text (self .landmark.name) .font(.headline) .lineLimit(0 ) Toggle (isOn: $userData .landmarks[self .landmarkIndex].isFavorite) { Text ("Favorite" ) } Divider () Text (self .landmark.park) .font(.caption) .bold() .lineLimit(0 ) Text (self .landmark.state) .font(.caption) } } } } struct WatchLandmarkDetail_Previews : PreviewProvider { static var previews: some View { let userData = UserData () return Group { WatchLandmarkDetail (landmark: userData.landmarks[0 ]).environmentObject(userData) .previewDevice("Apple Watch Series 4 - 44mm" ) WatchLandmarkDetail (landmark: userData.landmarks[1 ]).environmentObject(userData) .previewDevice("Apple Watch Series 2 - 38mm" ) } } }
步骤8
将scaleToFill()
更改为scaleToFit()
。
这将缩放圆图像以匹配显示器的宽度。
WatchLandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import SwiftUIstruct WatchLandmarkDetail : View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0 .id == landmark.id })! } var body: some View { ScrollView { VStack { CircleImage (image: self .landmark.image.resizable()) .scaledToFit() Text (self .landmark.name) .font(.headline) .lineLimit(0 ) Toggle (isOn: $userData .landmarks[self .landmarkIndex].isFavorite) { Text ("Favorite" ) } Divider () Text (self .landmark.park) .font(.caption) .bold() .lineLimit(0 ) Text (self .landmark.state) .font(.caption) } } } } struct WatchLandmarkDetail_Previews : PreviewProvider { static var previews: some View { let userData = UserData () return Group { WatchLandmarkDetail (landmark: userData.landmarks[0 ]).environmentObject(userData) .previewDevice("Apple Watch Series 4 - 44mm" ) WatchLandmarkDetail (landmark: userData.landmarks[1 ]).environmentObject(userData) .previewDevice("Apple Watch Series 2 - 38mm" ) } } }
步骤9
添加padding
,使地标名称在圆图像下方可见。
WatchLandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import SwiftUIstruct WatchLandmarkDetail : View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0 .id == landmark.id })! } var body: some View { ScrollView { VStack { CircleImage (image: self .landmark.image.resizable()) .scaledToFit() Text (self .landmark.name) .font(.headline) .lineLimit(0 ) Toggle (isOn: $userData .landmarks[self .landmarkIndex].isFavorite) { Text ("Favorite" ) } Divider () Text (self .landmark.park) .font(.caption) .bold() .lineLimit(0 ) Text (self .landmark.state) .font(.caption) } .padding(16 ) } } } struct WatchLandmarkDetail_Previews : PreviewProvider { static var previews: some View { let userData = UserData () return Group { WatchLandmarkDetail (landmark: userData.landmarks[0 ]).environmentObject(userData) .previewDevice("Apple Watch Series 4 - 44mm" ) WatchLandmarkDetail (landmark: userData.landmarks[1 ]).environmentObject(userData) .previewDevice("Apple Watch Series 2 - 38mm" ) } } }
步骤10
在后退
按钮上添加标题。
这会将后退按钮的文本设置为“地标”。但是,在添加“地标列表”视图之前,您不会看到“后退”按钮。
WatchLandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import SwiftUIstruct WatchLandmarkDetail : View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0 .id == landmark.id })! } var body: some View { ScrollView { VStack { CircleImage (image: self .landmark.image.resizable()) .scaledToFit() Text (self .landmark.name) .font(.headline) .lineLimit(0 ) Toggle (isOn: $userData .landmarks[self .landmarkIndex].isFavorite) { Text ("Favorite" ) } Divider () Text (self .landmark.park) .font(.caption) .bold() .lineLimit(0 ) Text (self .landmark.state) .font(.caption) } .padding(16 ) } .navigationBarTitle("Landmarks" ) } } struct WatchLandmarkDetail_Previews : PreviewProvider { static var previews: some View { let userData = UserData () return Group { WatchLandmarkDetail (landmark: userData.landmarks[0 ]).environmentObject(userData) .previewDevice("Apple Watch Series 4 - 44mm" ) WatchLandmarkDetail (landmark: userData.landmarks[1 ]).environmentObject(userData) .previewDevice("Apple Watch Series 2 - 38mm" ) } } }
第四节 添加watchOS地图视图
现在您已经创建了基本详情视图,现在是时候添加一个地图来显示地标的位置了。与CircleImage
不同,你不能重用iOS应用程序的MapView
。相反,您需要创建一个WKInterfaceObjectRepresentable
结构来包装WatchKit
的地图视图。
步骤1
向WatchKit extension
添加名为WatchMapView
的自定义视图。
WatchMapView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 import SwiftUIstruct WatchMapView : View { var body: some View { Text ("Hello World!" ) } } struct WatchMapView_Previews : PreviewProvider { static var previews: some View { WatchMapView () } }
步骤2
在WatchMapView
结构中,将View更改为WKInterfaceObjectRepresentable
。
要查看区别,请在步骤1和2所示的代码之间来回滚动。
WatchMapView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 import SwiftUIstruct WatchMapView : WKInterfaceObjectRepresentable { var body: some View { Text ("Hello World!" ) } } struct WatchMapView_Previews : PreviewProvider { static var previews: some View { WatchMapView () } }
Xcode显示编译错误,因为WatchMapView尚未实现WKInterfaceObjectRepresentable
协议属性。
步骤3
删除body()
方法并将其替换为landmark
属性。
无论何时创建地图视图,都需要传递此属性的值。例如,可以将landmark
的实例传递给预览。
WatchMapView.swift
1 2 3 4 5 6 7 8 9 10 11 import SwiftUIstruct WatchMapView : WKInterfaceObjectRepresentable { var landmark: Landmark } struct WatchMapView_Previews : PreviewProvider { static var previews: some View { WatchMapView (landmark: UserData ().landmarks[0 ]) } }
步骤4
实现WKInterfaceObjectRepresentable
的makeWKInterfaceObject(context:)
方法。
此方法用来创建WatchMapView
显示的WatchKit
地图。
WatchMapView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import SwiftUIstruct WatchMapView : WKInterfaceObjectRepresentable { var landmark: Landmark func makeWKInterfaceObject (context : WKInterfaceObjectRepresentableContext <WatchMapView >) -> WKInterfaceMap { return WKInterfaceMap () } } struct WatchMapView_Previews : PreviewProvider { static var previews: some View { WatchMapView (landmark: UserData ().landmarks[0 ]) } }
步骤5
通过实现WKInterfaceObjectRepresentable
的updateWKInterfaceObject(_:,context:)
方法,根据地标的坐标设置地图的区域。
现在,编译成功了。
WatchMapView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import SwiftUIstruct WatchMapView : WKInterfaceObjectRepresentable { var landmark: Landmark func makeWKInterfaceObject (context : WKInterfaceObjectRepresentableContext <WatchMapView >) -> WKInterfaceMap { return WKInterfaceMap () } func updateWKInterfaceObject (_ map : WKInterfaceMap , context : WKInterfaceObjectRepresentableContext <WatchMapView >) { let span = MKCoordinateSpan (latitudeDelta: 0.02 , longitudeDelta: 0.02 ) let region = MKCoordinateRegion ( center: landmark.locationCoordinate, span: span) map.setRegion(region) } } struct WatchMapView_Previews : PreviewProvider { static var previews: some View { WatchMapView (landmark: UserData ().landmarks[0 ]) } }
步骤6
选择WatchLandmarkView.swift
文件并将地图视图添加到VStack
的底部。
代码添加了一个分隔符,后跟地图视图。.scaledToFit()
和.padding()
修饰符将地图以合适的大小适配屏幕。
WatchLandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import SwiftUIstruct WatchLandmarkDetail : View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0 .id == landmark.id })! } var body: some View { ScrollView { VStack { CircleImage (image: self .landmark.image.resizable()) .scaledToFit() Text (self .landmark.name) .font(.headline) .lineLimit(0 ) Toggle (isOn: $userData .landmarks[self .landmarkIndex].isFavorite) { Text ("Favorite" ) } Divider () Text (self .landmark.park) .font(.caption) .bold() .lineLimit(0 ) Text (self .landmark.state) .font(.caption) Divider () WatchMapView (landmark: self .landmark) .scaledToFit() .padding() } .padding(16 ) } .navigationBarTitle("Landmarks" ) } } struct WatchLandmarkDetail_Previews : PreviewProvider { static var previews: some View { let userData = UserData () return Group { WatchLandmarkDetail (landmark: userData.landmarks[0 ]).environmentObject(userData) .previewDevice("Apple Watch Series 4 - 44mm" ) WatchLandmarkDetail (landmark: userData.landmarks[1 ]).environmentObject(userData) .previewDevice("Apple Watch Series 2 - 38mm" ) } } }
第五节 创建跨平台列表视图
对于地标列表,您可以重用iOS应用程序中的行Row
,但每个平台都需要呈现自己的详情视图。为了支持这一点,您将把LandmarkList视图转换为泛型列表类型,在这里实例化代码定义了详情视图。
步骤1 在工具栏中,选择Landmarks scheme
。
Xcode现在编译并运行应用程序的iOS版本。在将列表移动到watchOS应用程序之前,您要确保对LandmarkList
视图的任何修改在iOS应用程序中仍然有效。
步骤2
选择LandmarkList.swift
并更改类型声明,使其成为泛型类型。
LandmarksList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import SwiftUIstruct LandmarkList <DetailView : View >: View { @EnvironmentObject private var userData: UserData var body: some View { List { Toggle (isOn: $userData .showFavoritesOnly) { Text ("Show Favorites Only" ) } ForEach (userData.landmarks) { landmark in if ! self .userData.showFavoritesOnly || landmark.isFavorite { NavigationLink ( destination: LandmarkDetail (landmark: landmark).environmentObject(self .userData)) { LandmarkRow (landmark: landmark) } } } } .navigationBarTitle(Text ("Landmarks" )) } } struct LandmarksList_Previews : PreviewProvider { static var previews: some View { ForEach (["iPhone SE" , "iPhone XS Max" ].identified(by: \.self )) { deviceName in LandmarkList () .previewDevice(PreviewDevice (rawValue: deviceName)) .previewDisplayName(deviceName) } .environmentObject(UserData ()) } }
在创建LandmarkList
结构的实例时,添加泛型声明会导致Generic parameter could not be inferred(无法推断泛型参数)
错误。以下步骤会修复这些错误。
步骤3
为创建局部视图的闭包添加属性。
LandmarksList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import SwiftUIstruct LandmarkList <DetailView : View >: View { @EnvironmentObject private var userData: UserData let detailViewProducer: (Landmark ) -> DetailView var body: some View { List { Toggle (isOn: $userData .showFavoritesOnly) { Text ("Show Favorites Only" ) } ForEach (userData.landmarks) { landmark in if ! self .userData.showFavoritesOnly || landmark.isFavorite { NavigationLink ( destination: LandmarkDetail (landmark: landmark).environmentObject(self .userData)) { LandmarkRow (landmark: landmark) } } } } .navigationBarTitle(Text ("Landmarks" )) } } struct LandmarksList_Previews : PreviewProvider { static var previews: some View { ForEach (["iPhone SE" , "iPhone XS Max" ].identified(by: \.self )) { deviceName in LandmarkList () .previewDevice(PreviewDevice (rawValue: deviceName)) .previewDisplayName(deviceName) } .environmentObject(UserData ()) } }
步骤4
使用detailViewProducer
属性为地标创建详情视图。
LandmarksList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import SwiftUIstruct LandmarkList <DetailView : View >: View { @EnvironmentObject private var userData: UserData let detailViewProducer: (Landmark ) -> DetailView var body: some View { List { Toggle (isOn: $userData .showFavoritesOnly) { Text ("Show Favorites Only" ) } ForEach (userData.landmarks) { landmark in if ! self .userData.showFavoritesOnly || landmark.isFavorite { NavigationLink ( destination: self .detailViewProducer(landmark).environmentObject(self .userData)) { LandmarkRow (landmark: landmark) } } } } .navigationBarTitle(Text ("Landmarks" )) } } struct LandmarksList_Previews : PreviewProvider { static var previews: some View { ForEach (["iPhone SE" , "iPhone XS Max" ].identified(by: \.self )) { deviceName in LandmarkList () .previewDevice(PreviewDevice (rawValue: deviceName)) .previewDisplayName(deviceName) } .environmentObject(UserData ()) } }
创建LandmarkList
实例时,还需要提供一个闭包,用于创建landmark
的详情视图。
步骤5
选择Home.swift。在CategoryHome
结构的body()
方法中,添加闭包以创建LandmarkDetail
视图。
Xcode根据闭包的返回值推断LandmarkList
结构的泛型类型。
Home.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import SwiftUIstruct CategoryHome : View { var categories: [String : [Landmark ]] { Dictionary ( grouping: landmarkData, by: { $0 .category.rawValue } ) } var featured: [Landmark ] { landmarkData.filter { $0 .isFeatured } } @State var showingProfile = false var profileButton: some View { Button (action: { self .showingProfile.toggle() }) { Image (systemName: "person.crop.circle" ) .imageScale(.large) .accessibility(label: Text ("User Profile" )) .padding() } } var body: some View { NavigationView { List { FeaturedLandmarks (landmarks: featured) .scaledToFill() .frame(height: CGFloat (200 )) .clipped() .listRowInsets(EdgeInsets ()) ForEach (categories.keys.sorted(), id: \.self ) { key in CategoryRow (categoryName: key, items: self .categories[key]! ) } .listRowInsets(EdgeInsets ()) NavigationLink (destination: LandmarkList { LandmarkDetail (landmark: $0 ) }) { Text ("See All" ) } } .navigationBarTitle(Text ("Featured" )) .navigationBarItems(trailing: profileButton) .sheet(isPresented: $showingProfile ) { ProfileHost () } } } } struct FeaturedLandmarks : View { var landmarks: [Landmark ] var body: some View { landmarks[0 ].image.resizable() } } struct CategoryHome_Previews : PreviewProvider { static var previews: some View { CategoryHome () .environmentObject(UserData ()) } }
步骤6
在LandmarkList.swift中,向预览添加类似的代码。
在这里,您需要使用条件判断,根据Xcode编译的当前scheme
定义详情视图。现在地标应用程序可以按照预期在iOS上构建和运行了。
LandmarksList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import SwiftUIstruct LandmarkList <DetailView : View >: View { @EnvironmentObject private var userData: UserData let detailViewProducer: (Landmark ) -> DetailView var body: some View { List { Toggle (isOn: $userData .showFavoritesOnly) { Text ("Show Favorites Only" ) } ForEach (userData.landmarks) { landmark in if ! self .userData.showFavoritesOnly || landmark.isFavorite { NavigationLink ( destination: self .detailViewProducer(landmark).environmentObject(self .userData)) { LandmarkRow (landmark: landmark) } } } } .navigationBarTitle(Text ("Landmarks" )) } } #if os(watchOS)typealias PreviewDetailView = WatchLandmarkDetail #else typealias PreviewDetailView = LandmarkDetail #endif struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList { PreviewDetailView (landmark: $0 ) } .environmentObject(UserData ()) } }
第六节 添加地标列表
现在您已经更新了LandmarksList视图,以便它在两个平台上都能工作,您可以将其添加到watchOS应用程序中。
步骤1
在文件检查器中,将LandmarksList.swift
添加到WatchLandmarks Extension
Target。
现在可以在watchOS应用程序的代码中使用LandmarkList视图。
步骤2
在工具栏中,将scheme
更改为WatchLandmarks
。
步骤3
打开LandmarkList.swift
,继续预览。
预览现在显示watchOS列表视图。
LandmarksList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import SwiftUIstruct LandmarkList <DetailView : View >: View { @EnvironmentObject private var userData: UserData let detailViewProducer: (Landmark ) -> DetailView var body: some View { List { Toggle (isOn: $userData .showFavoritesOnly) { Text ("Show Favorites Only" ) } ForEach (userData.landmarks) { landmark in if ! self .userData.showFavoritesOnly || landmark.isFavorite { NavigationLink ( destination: self .detailViewProducer(landmark).environmentObject(self .userData)) { LandmarkRow (landmark: landmark) } } } } .navigationBarTitle(Text ("Landmarks" )) } } #if os(watchOS)typealias PreviewDetailView = WatchLandmarkDetail #else typealias PreviewDetailView = LandmarkDetail #endif struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList { PreviewDetailView (landmark: $0 ) } .environmentObject(UserData ()) } }
watchOS应用程序的根视图是ContentView,它显示默认文本Hello World!
。
步骤4
修改ContentView,使其显示列表视图。
ContentView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import SwiftUIstruct ContentView : View { var body: some View { LandmarkList { WatchLandmarkDetail (landmark: $0 ) } .environmentObject(UserData ()) } } struct ContentView_Previews : PreviewProvider { static var previews: some View { LandmarkList { WatchLandmarkDetail (landmark: $0 ) } .environmentObject(UserData ()) } }
步骤5
在模拟器中编译运行watchOS应用程序。
通过滚动列表中的地标来测试watchOS应用程序的行为,点击以查看地标的详细信息,并将其标记为收藏夹。单击back
按钮返回列表,然后打开Favorite
开关,这样您只能看到收藏的地标。
第7节 创建自定义通知界面
你的watchOS版地标应用程序几乎完成了。在这最后一部分中,您将创建一个通知界面,每当您离最喜欢的某一位置很近时,会收到地标信息的通知信息。
注意:本节仅介绍如何在收到通知后显示通知。它不描述如何设置或发送通知。
步骤1
打开NotificationView.swift
并创建一个显示有关地标、标题和消息的信息的视图。
因为任何通知值都可以为nil,所以预览将显示通知视图的两个版本。第一个仅在未提供数据时显示默认值,第二个显示您提供的标题、消息和位置。
NotificationView.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import SwiftUIstruct NotificationView : View { let title: String ? let message: String ? let landmark: Landmark ? init (title : String ? = nil , message : String ? = nil , landmark : Landmark ? = nil ) { self .title = title self .message = message self .landmark = landmark } var body: some View { VStack { if landmark != nil { CircleImage (image: landmark! .image.resizable()) .scaledToFit() } Text (title ?? "Unknown Landmark" ) .font(.headline) .lineLimit(0 ) Divider () Text (message ?? "You are within 5 miles of one of your favorite landmarks." ) .font(.caption) .lineLimit(0 ) } } } struct NotificationView_Previews : PreviewProvider { static var previews: some View { Group { NotificationView () NotificationView (title: "Turtle Rock" , message: "You are within 5 miles of Turtle Rock." , landmark: UserData ().landmarks[0 ]) } .previewLayout(.sizeThatFits) } }
步骤2
打开NotificationController
并添加landmark
、title
和message
属性。
这些数据存储了有关通知传入信息。
NotificationController.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import WatchKitimport SwiftUIimport UserNotificationsclass NotificationController : WKUserNotificationHostingController <NotificationView > { var landmark: Landmark ? var title: String ? var message: String ? override var body: NotificationView { NotificationView () } override func willActivate () { super .willActivate() } override func didDeactivate () { super .didDeactivate() } override func didReceive (_ notification : UNNotification ) { } }
步骤3
更新body()
方法以使用这些属性。
此方法实例化您先前创建的通知视图。
NotificationController.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import WatchKitimport SwiftUIimport UserNotificationsclass NotificationController : WKUserNotificationHostingController <NotificationView > { var landmark: Landmark ? var title: String ? var message: String ? override var body: NotificationView { NotificationView (title: title, message: message, landmark: landmark) } override func willActivate () { super .willActivate() } override func didDeactivate () { super .didDeactivate() } override func didReceive (_ notification : UNNotification ) { } }
步骤4
定义LandmarkIndexKey
。
使用此键可从通知中提取地标索引。
NotificationController.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import WatchKitimport SwiftUIimport UserNotificationsclass NotificationController : WKUserNotificationHostingController <NotificationView > { var landmark: Landmark ? var title: String ? var message: String ? let landmarkIndexKey = "landmarkIndex" override var body: NotificationView { NotificationView (title: title, message: message, landmark: landmark) } override func willActivate () { super .willActivate() } override func didDeactivate () { super .didDeactivate() } override func didReceive (_ notification : UNNotification ) { } }
步骤5
更新didReceive(:)
方法以解析通知中的数据。
此方法更新控制器的属性。调用此方法后,系统将使控制器的body属性无效,该属性将更新导航视图。然后系统在Apple Watch
上显示通知。
NotificationController.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import WatchKitimport SwiftUIimport UserNotificationsclass NotificationController : WKUserNotificationHostingController <NotificationView > { var landmark: Landmark ? var title: String ? var message: String ? let landmarkIndexKey = "landmarkIndex" override var body: NotificationView { NotificationView (title: title, message: message, landmark: landmark) } override func willActivate () { super .willActivate() } override func didDeactivate () { super .didDeactivate() } override func didReceive (_ notification : UNNotification ) { let userData = UserData () let notificationData = notification.request.content.userInfo as? [String : Any ] let aps = notificationData? ["aps" ] as? [String : Any ] let alert = aps? ["alert" ] as? [String : Any ] title = alert? ["title" ] as? String message = alert? ["body" ] as? String if let index = notificationData? [landmarkIndexKey] as? Int { landmark = userData.landmarks[index] } } }
当Apple Watch收到通知时,它会创建与通知类别关联的通知控制器。若要设置通知控制器的类别,必须打开并编辑应用程序的情节提要。
步骤6
在项目导航栏中,选择“Watch Landmarks”文件夹并打开界面storyboard
。选择指向静态通知界面控制器的箭头。
步骤7
在属性检查器中,将Notification Category
的名称设置为LandmarkNear
。
配置测试负载来使用LandmarkNear类别,并传递通知控制器期望的数据。
步骤8
选择PushNotificationPayload.apns
文件,并更新title、body、category和landmarkIndex属性。请务必将类别设置为LandmarkNear
。您还可以删除本教程中未使用的任何键,如subtitle
、WatchKit Simulator Actions
和customKey
。
PushNotificationPayload.apns
1 2 3 4 5 6 7 8 9 10 11 12 { "aps": { "alert": { "body": "You are within 5 miles of Silver Salmon Creek." "title": "Silver Salmon Creek", }, "category": "LandmarkNear", "thread-id": "5280" }, "landmarkIndex": 1 }
负载文件模拟远程通知中从服务器发送的数据。
步骤9
选择“Landmarks-Watch (Notification)” scheme,并编译和运行您的应用程序。
第一次运行通知Scheme时,系统将请求发送通知的权限。选择Allow(允许)
。模拟器随后显示一个可滚动的通知,其中包括:一个用于将地标应用程序标记为发送者的框、通知视图和通知操作的按钮。
应用程序设计和布局
使用UI控件
在地标应用程序中,用户可以创建一个个人资料页来表达他们的个性。为了让用户能够更改他们的个人简介,您将添加一个编辑模式,并设计一个偏好设置页面。
您将使用各种用于数据输入的通用用户界面控件,并在用户保存更改时更新地标数据模型。
学习时间:25分钟
下载地址:WorkingWithUIControls.zip
第一节 显示用户简介
Landmarks
应用程序在本地存储一些详情配置和偏好设置。在用户编辑其详情之前,它们将显示在没有任何编辑控件的摘要视图中。
步骤1
要开始,请在Landmarks
目录下创建一个名为Profile
的新目录,然后将名为ProfileHost
的视图添加到该目录中。
ProfileHost
视图将同时承载用户信息的静态摘要视图和编辑模式。
ProfileHost.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import SwiftUIstruct ProfileHost : View { @State var draftProfile = Profile .default var body: some View { Text ("Profile for: \(draftProfile.username) " ) } } struct ProfileHost_Previews : PreviewProvider { static var previews: some View { ProfileHost () } }
步骤2
将Home.swift
中的静态文本替换为上一步中创建的ProfileHost
。
现在,主屏幕上的profile
按钮将以模态方式展现用户简介。
Home.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import SwiftUIstruct CategoryHome : View { var categories: [String : [Landmark ]] { Dictionary ( grouping: landmarkData, by: { $0 .category.rawValue } ) } var featured: [Landmark ] { landmarkData.filter { $0 .isFeatured } } @State var showingProfile = false @EnvironmentObject var userData: UserData var profileButton: some View { Button (action: { self .showingProfile.toggle() }) { Image (systemName: "person.crop.circle" ) .imageScale(.large) .accessibility(label: Text ("User Profile" )) .padding() } } var body: some View { NavigationView { List { FeaturedLandmarks (landmarks: featured) .scaledToFill() .frame(height: 200 ) .clipped() .listRowInsets(EdgeInsets ()) ForEach (categories.keys.sorted(), id: \.self ) { key in CategoryRow (categoryName: key, items: self .categories[key]! ) } .listRowInsets(EdgeInsets ()) NavigationLink (destination: LandmarkList ()) { Text ("See All" ) } } .navigationBarTitle(Text ("Featured" )) .navigationBarItems(trailing: profileButton) .sheet(isPresented: $showingProfile ) { ProfileHost () .environmentObject(self .userData) } } } } struct FeaturedLandmarks : View { var landmarks: [Landmark ] var body: some View { landmarks[0 ].image.resizable() } } struct CategoryHome_Previews : PreviewProvider { static var previews: some View { CategoryHome () } }
步骤3
创建一个名为ProfileSummary
的新视图,该视图接受一个Profile
实例并显示一些基本的用户信息。
ProfileSummary
持有一个Profile
,比个人简介持有它好,因为父视图ProfileHost
管理此视图的State
。
ProfileSummary.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import SwiftUIstruct ProfileSummary : View { var profile: Profile static let goalFormat: DateFormatter = { let formatter = DateFormatter () formatter.dateStyle = .long formatter.timeStyle = .none return formatter }() var body: some View { List { Text (profile.username) .bold() .font(.title) Text ("Notifications: \(self .profile.prefersNotifications ? "On" : "Off" ) " ) Text ("Seasonal Photos: \(self .profile.seasonalPhoto.rawValue) " ) Text ("Goal Date: \(self .profile.goalDate, formatter: Self .goalFormat) " ) } } } struct ProfileSummary_Previews : PreviewProvider { static var previews: some View { ProfileSummary (profile: Profile .default) } }
步骤4
更新ProfileHost
以显示新的摘要视图。
ProfileHost.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import SwiftUIstruct ProfileHost : View { @State var draftProfile = Profile .default var body: some View { VStack (alignment: .leading, spacing: 20 ) { ProfileSummary (profile: draftProfile) } .padding() } } struct ProfileHost_Previews : PreviewProvider { static var previews: some View { ProfileHost () } }
步骤5
创建一个名为HikeBadge
的新视图,该视图由绘制路径和形状 中制作的徽章以及徒步旅行的一些数据文本组成。
徽章只是一个图形,因此HikeBadge
中的文本和accessibility(label:)
修饰符使徽章的含义对其他用户更清晰。
注意:
两次调用frame(width:height:)
修饰符,使徽章以其设计时的尺寸300×300点进行缩放渲染。
HikeBadge.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import SwiftUIstruct HikeBadge : View { var name: String var body: some View { VStack (alignment: .center) { Badge () .frame(width: 300 , height: 300 ) .scaleEffect(1.0 / 3.0 ) .frame(width: 100 , height: 100 ) Text (name) .font(.caption) .accessibility(label: Text ("Badge for \(name) ." )) } } } struct HikeBadge_Previews : PreviewProvider { static var previews: some View { HikeBadge (name: "Preview Testing" ) } }
步骤6
更新ProfileSummary
以添加不同颜色的徽章以及获得徽章的原因文字。
ProfileSummary.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import SwiftUIstruct ProfileSummary : View { var profile: Profile static let goalFormat: DateFormatter = { let formatter = DateFormatter () formatter.dateStyle = .long formatter.timeStyle = .none return formatter }() var body: some View { List { Text (profile.username) .bold() .font(.title) Text ("Notifications: \(self .profile.prefersNotifications ? "On" : "Off" ) " ) Text ("Seasonal Photos: \(self .profile.seasonalPhoto.rawValue) " ) Text ("Goal Date: \(self .profile.goalDate, formatter: Self .goalFormat) " ) VStack (alignment: .leading) { Text ("Completed Badges" ) .font(.headline) ScrollView { HStack { HikeBadge (name: "First Hike" ) HikeBadge (name: "Earth Day" ) .hueRotation(Angle (degrees: 90 )) HikeBadge (name: "Tenth Hike" ) .grayscale(0.5 ) .hueRotation(Angle (degrees: 45 )) } } .frame(height: 140 ) } } } } struct ProfileSummary_Previews : PreviewProvider { static var previews: some View { ProfileSummary (profile: Profile .default) } }
步骤7
通过引入视图动画与转场 的HikeView
来完成ProfileSummary
。
ProfileSummary.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import SwiftUIstruct ProfileSummary : View { var profile: Profile static let goalFormat: DateFormatter = { let formatter = DateFormatter () formatter.dateStyle = .long formatter.timeStyle = .none return formatter }() var body: some View { List { Text (profile.username) .bold() .font(.title) Text ("Notifications: \(self .profile.prefersNotifications ? "On" : "Off" ) " ) Text ("Seasonal Photos: \(self .profile.seasonalPhoto.rawValue) " ) Text ("Goal Date: \(self .profile.goalDate, formatter: Self .goalFormat) " ) VStack (alignment: .leading) { Text ("Completed Badges" ) .font(.headline) ScrollView { HStack { HikeBadge (name: "First Hike" ) HikeBadge (name: "Earth Day" ) .hueRotation(Angle (degrees: 90 )) HikeBadge (name: "Tenth Hike" ) .grayscale(0.5 ) .hueRotation(Angle (degrees: 45 )) } } .frame(height: 140 ) } VStack (alignment: .leading) { Text ("Recent Hikes" ) .font(.headline) HikeView (hike: hikeData[0 ]) } } } } struct ProfileSummary_Previews : PreviewProvider { static var previews: some View { ProfileSummary (profile: Profile .default) } }
第二节 添加编辑模式
用户需要在查看或编辑其简介详情之间切换。您将添加一个编辑模式,通过向现有的ProfileHost
添加一个EditButton
,然后创建一个带有控件的视图,用于编辑单个数据。
步骤1
添加一个Environment
属性,并设置\.editMode
。
可以使用此属性读取和写入当前编辑范围。
ProfileHost.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import SwiftUIstruct ProfileHost : View { @Environment (\.editMode) var mode @State var draftProfile = Profile .default var body: some View { VStack (alignment: .leading, spacing: 20 ) { ProfileSummary (profile: draftProfile) } .padding() } } struct ProfileHost_Previews : PreviewProvider { static var previews: some View { ProfileHost () } }
步骤2
创建一个编辑按钮,用于打开和关闭环境的编辑模式。
ProfileHost.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import SwiftUIstruct ProfileHost : View { @Environment (\.editMode) var mode @State var draftProfile = Profile .default var body: some View { VStack (alignment: .leading, spacing: 20 ) { HStack { Spacer () EditButton () } ProfileSummary (profile: draftProfile) } .padding() } } struct ProfileHost_Previews : PreviewProvider { static var previews: some View { ProfileHost () } }
步骤3
更新UserData
类以包含用户简介的实例,该实例在用户关闭简介视图后仍然存在。
UserData.swift
1 2 3 4 5 6 7 8 import Combineimport SwiftUIfinal class UserData : ObservableObject { @Published var showFavoritesOnly = false @Published var landmarks = landmarkData @Published var profile = Profile .default }
步骤4
从Environment
中读取Profile
数据,将数据的控制权传递给ProfileHost
。
为了避免在确认编辑之前更新全局应用程序状态(例如当用户输入其名称时),编辑视图将对其自身的副本进行操作。
ProfileHost.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import SwiftUIstruct ProfileHost : View { @Environment (\.editMode) var mode @EnvironmentObject var userData: UserData @State var draftProfile = Profile .default var body: some View { VStack (alignment: .leading, spacing: 20 ) { HStack { Spacer () EditButton () } ProfileSummary (profile: draftProfile) } .padding() } } struct ProfileHost_Previews : PreviewProvider { static var previews: some View { ProfileHost () } }
步骤5
添加条件视图,显示静态简介视图或编辑模式视图。
注意
目前,编辑模式只是一个静态文本字段。
ProfileHost.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import SwiftUIstruct ProfileHost : View { @Environment (\.editMode) var mode @EnvironmentObject var userData: UserData @State var draftProfile = Profile .default var body: some View { VStack (alignment: .leading, spacing: 20 ) { HStack { Spacer () EditButton () } if self .mode? .wrappedValue == .inactive { ProfileSummary (profile: userData.profile) } else { Text ("Profile Editor" ) } } .padding() } } struct ProfileHost_Previews : PreviewProvider { static var previews: some View { ProfileHost () } }
第三节 定义用户简介编辑器
用户简介编辑器主要由不同的控件组成,这些控件更改用户简介中的各个详情信息。配置文件中的某些项目(如徽章)不可由用户编辑,因此它们不会显示在编辑器中。
为了与信息摘要保持一致,您将在编辑器中按相同的顺序添加概要文件详情信息。
步骤1
创建一个名为ProfileEditor
的新视图,并包含对用户简介副本的绑定。
视图中的第一个控件是一个TextField
,它控制并更新一个字符串的绑定,是用户选择的显示名称。当创建TextField
时,您需要提供标签和字符串的绑定。
ProfileEditor.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import SwiftUIstruct ProfileEditor : View { @Binding var profile: Profile var body: some View { List { HStack { Text ("Username" ).bold() Divider () TextField ("Username" , text: $profile .username) } } } } struct ProfileEditor_Previews : PreviewProvider { static var previews: some View { ProfileEditor (profile: .constant(.default)) } }
步骤2
更新ProfileHost
中的条件内容,使其包含Profile Editor
,并传递简介信息的绑定。
现在,单击“Edit”时将显示“简介编辑视图”。
ProfileHost.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import SwiftUIstruct ProfileHost : View { @Environment (\.editMode) var mode @EnvironmentObject var userData: UserData @State var draftProfile = Profile .default var body: some View { VStack (alignment: .leading, spacing: 20 ) { HStack { Spacer () EditButton () } if self .mode? .wrappedValue == .inactive { ProfileSummary (profile: userData.profile) } else { ProfileEditor (profile: $draftProfile ) } } .padding() } } struct ProfileHost_Previews : PreviewProvider { static var previews: some View { ProfileHost () } }
步骤3
添加是否接收地标相关事件通知的开关。
Toggles
是只有打开或关闭的控件,因此它们非常适合布尔值Boolean
,如“yes”或“no”的设置。
ProfileEditor.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import SwiftUIstruct ProfileEditor : View { @Binding var profile: Profile var body: some View { List { HStack { Text ("Username" ).bold() Divider () TextField ("Username" , text: $profile .username) } Toggle (isOn: $profile .prefersNotifications) { Text ("Enable Notifications" ) } } } } struct ProfileEditor_Previews : PreviewProvider { static var previews: some View { ProfileEditor (profile: .constant(.default)) } }
步骤4
将Picker
控件及其标签放置在VStack
中,使地标照片具有可选择的季节。
ProfileEditor.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import SwiftUIstruct ProfileEditor : View { @Binding var profile: Profile var body: some View { List { HStack { Text ("Username" ).bold() Divider () TextField ("Username" , text: $profile .username) } Toggle (isOn: $profile .prefersNotifications) { Text ("Enable Notifications" ) } VStack (alignment: .leading, spacing: 20 ) { Text ("Seasonal Photo" ).bold() Picker ("Seasonal Photo" , selection: $profile .seasonalPhoto) { ForEach (Profile .Season .allCases, id: \.self ) { season in Text (season.rawValue).tag(season) } } .pickerStyle(SegmentedPickerStyle ()) } .padding(.top) } } } struct ProfileEditor_Previews : PreviewProvider { static var previews: some View { ProfileEditor (profile: .constant(.default)) } }
步骤5
最后,在季节选择器下面添加一个DatePicker
,修改到达地标的日期。
ProfileEditor.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import SwiftUIstruct ProfileEditor : View { @Binding var profile: Profile var dateRange: ClosedRange <Date > { let min = Calendar .current.date(byAdding: .year, value: - 1 , to: profile.goalDate)! let max = Calendar .current.date(byAdding: .year, value: 1 , to: profile.goalDate)! return min... max } var body: some View { List { HStack { Text ("Username" ).bold() Divider () TextField ("Username" , text: $profile .username) } Toggle (isOn: $profile .prefersNotifications) { Text ("Enable Notifications" ) } VStack (alignment: .leading, spacing: 20 ) { Text ("Seasonal Photo" ).bold() Picker ("Seasonal Photo" , selection: $profile .seasonalPhoto) { ForEach (Profile .Season .allCases, id: \.self ) { season in Text (season.rawValue).tag(season) } } .pickerStyle(SegmentedPickerStyle ()) } .padding(.top) VStack (alignment: .leading, spacing: 20 ) { Text ("Goal Date" ).bold() DatePicker ( "Goal Date" , selection: $profile .goalDate, in: dateRange, displayedComponents: .date) } .padding(.top) } } } struct ProfileEditor_Previews : PreviewProvider { static var previews: some View { ProfileEditor (profile: .constant(.default)) } }
第四节 延迟编辑传递
要使其编辑,直到用户退出编辑模式后才生效,在编辑过程中使用其Profile
的草稿副本,然后仅当用户确认编辑时才将草稿副本分配给真实副本。
步骤1
向ProfileHost
添加取消按钮。
与EditButton
提供的Done
按钮不同,Cancel
按钮不会将编辑应用于其闭包中的真实Profile
数据。
ProfileHost.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import SwiftUIstruct ProfileHost : View { @Environment (\.editMode) var mode @EnvironmentObject var userData: UserData @State var draftProfile = Profile .default var body: some View { VStack (alignment: .leading, spacing: 20 ) { HStack { if self .mode? .wrappedValue == .active { Button ("Cancel" ) { self .draftProfile = self .userData.profile self .mode? .animation().wrappedValue = .inactive } } Spacer () EditButton () } if self .mode? .wrappedValue == .inactive { ProfileSummary (profile: userData.profile) } else { ProfileEditor (profile: $draftProfile ) } } .padding() } } struct ProfileHost_Previews : PreviewProvider { static var previews: some View { ProfileHost () } }
步骤2
应用onAppear(perform:)
和onDisappear(perform:)
修饰符,将正确的用户简介数据填充给编辑器,并在用户点击Done
按钮时更新简介数据。
否则,在下次激活编辑模式时显示旧值。
ProfileHost.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import SwiftUIstruct ProfileHost : View { @Environment (\.editMode) var mode @EnvironmentObject var userData: UserData @State var draftProfile = Profile .default var body: some View { VStack (alignment: .leading, spacing: 20 ) { HStack { if self .mode? .wrappedValue == .active { Button ("Cancel" ) { self .draftProfile = self .userData.profile self .mode? .animation().wrappedValue = .inactive } } Spacer () EditButton () } if self .mode? .wrappedValue == .inactive { ProfileSummary (profile: userData.profile) } else { ProfileEditor (profile: $draftProfile ) .onAppear { self .draftProfile = self .userData.profile } .onDisappear { self .userData.profile = self .draftProfile } } } .padding() } } struct ProfileHost_Previews : PreviewProvider { static var previews: some View { ProfileHost () } }
处理用户输入
在Landmarks
应用程序中,用户可以标记他们最喜欢的位置,并筛选列表来仅仅显示他们最喜欢的位置。要创建此功能,首先要向列表中添加一个开关,以便用户只关注他们的收藏夹,然后添加一个星形按钮,用户点击该按钮可将地标标记到收藏夹。
学习时间:20分钟
下载示例:HandlingUserInput.zip
第一节 标记用户最喜欢的地标
从优化列表开始,让用户一目了然地看到他们的最爱。为每个显示最喜欢的地标行添加一个星。
步骤1
打开Xcode项目,然后在项目导航器中选择LandmarkRow.swift
。
步骤2
在Spacer()
之后,在if
语句中添加一个星星Image
,用来测试当前地标是否被收藏。
在SwiftUI
语句块中,使用if
语句有条件地包含视图。
LandmarkRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import SwiftUIstruct LandmarkRow : View { var landmark: Landmark var body: some View { HStack { landmark.image .resizable() .frame(width: 50 , height: 50 ) Text (landmark.name) Spacer () if landmark.isFavorite { Image (systemName: "star.fill" ) .imageScale(.medium) } } } } struct LandmarkRow_Previews : PreviewProvider { static var previews: some View { Group { LandmarkRow (landmark: landmarkData[0 ]) LandmarkRow (landmark: landmarkData[1 ]) } .previewLayout(.fixed(width: 300 , height: 70 )) } }
步骤3
由于系统图像是基于矢量的,因此可以使用foregroundColor(_:)
修改器更改其颜色。
当地标的isFavorite
属性为true
时,星星就出现了。您将在本教程后面看到如何修改该属性。
LandmarkRow.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import SwiftUIstruct LandmarkRow : View { var landmark: Landmark var body: some View { HStack { landmark.image .resizable() .frame(width: 50 , height: 50 ) Text (landmark.name) Spacer () if landmark.isFavorite { Image (systemName: "star.fill" ) .imageScale(.medium) .foregroundColor(.yellow) } } } } struct LandmarkRow_Previews : PreviewProvider { static var previews: some View { Group { LandmarkRow (landmark: landmarkData[0 ]) LandmarkRow (landmark: landmarkData[1 ]) } .previewLayout(.fixed(width: 300 , height: 70 )) } }
第二节 筛选列表视图
您可以自定义列表视图,使其显示所有地标,或仅显示用户的收藏夹。为此,需要向LandmarkList
类型添加@State
。
@State
是一个值或一组值,可以随时间变化,并影响视图的行为、内容或布局。使用带有@State
的属性将其添加到视图中。
步骤1
在项目导航器中选择LandmarkList.swift
。将名为showFavoritesOnly
的@State
属性添加到LandmarkList
,其初始值设置为false
。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import SwiftUIstruct LandmarkList : View { @State var showFavoritesOnly = false var body: some View { NavigationView { List (landmarkData) { landmark in NavigationLink (destination: LandmarkDetail (landmark: landmark)) { LandmarkRow (landmark: landmark) } } .navigationBarTitle(Text ("Landmarks" )) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () } }
步骤2
单击“Resume”按钮刷新画布。
当您对视图的结构进行更改(如添加或修改属性)时,需要手动刷新画布。
步骤3
通过检查showFavoritesOnly
属性和每个landmark.isFavorite
值筛选地标列表。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import SwiftUIstruct LandmarkList : View { @State var showFavoritesOnly = false var body: some View { NavigationView { List (landmarkData) { landmark in if ! self .showFavoritesOnly || landmark.isFavorite { NavigationLink (destination: LandmarkDetail (landmark: landmark)) { LandmarkRow (landmark: landmark) } } } .navigationBarTitle(Text ("Landmarks" )) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () } }
第三节 添加控件以切换状态
要让用户控制列表的筛选器,需要添加一个控件,该控件可以单独更改showFavoritesOnly
的值。通过绑定toggle
控件来完成此操作。
绑定是对可变状态的引用。当用户从关闭切换到打开,然后再次关闭时,控件使用绑定相应地更新视图的状态。
步骤1
将行嵌套到ForEach
中。
若要在列表中组合静态视图和动态视图,或组合两个或多个不同的动态视图组,请使用ForEach
类型,而不是将数据集合传递给列表。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import SwiftUIstruct LandmarkList : View { @State var showFavoritesOnly = true var body: some View { NavigationView { List { ForEach (landmarkData) { landmark in if ! self .showFavoritesOnly || landmark.isFavorite { NavigationLink (destination: LandmarkDetail (landmark: landmark)) { LandmarkRow (landmark: landmark) } } } } .navigationBarTitle(Text ("Landmarks" )) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () } }
步骤2
添加一个Toggle
视图作为列表视图的第一个子视图,给showFavoritesOnly
做一个绑定。
您可以使用$
来访问状态变量或其绑定的属性。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import SwiftUIstruct LandmarkList : View { @State var showFavoritesOnly = true var body: some View { NavigationView { List { Toggle (isOn: $showFavoritesOnly ) { Text ("Favorites only" ) } ForEach (landmarkData) { landmark in if ! self .showFavoritesOnly || landmark.isFavorite { NavigationLink (destination: LandmarkDetail (landmark: landmark)) { LandmarkRow (landmark: landmark) } } } } .navigationBarTitle(Text ("Landmarks" )) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () } }
步骤3
使用实时预览并通过点击切换来尝试此新功能。
第四节 使用Observable Object进行存储
为了让用户控制哪些特定的地标是最喜欢的,您首先要将地标数据存储在一个Observable Object
中。
Observable Object
的自定义对象,可以从SwiftUI
环境中的存储绑定到视图。SwiftUI
监视可观察对象的任何可能影响视图的更改,并在更改后显示正确的视图。
步骤1
创建一个名为UserData.Swift
的新Swift
文件。
UserData.swift
步骤2
从组合框架中声明遵守ObservableObject
协议的新模型类型。
SwiftUI
订阅您的ObservableObject
,并在数据更改时更新任何需要刷新的视图。
UserData.swift
1 2 3 4 5 6 import SwiftUIimport Combinefinal class UserData : ObservableObject {}
步骤3
添加showFavoritesOnly
和地标的存储属性及其初始值。
UserData.swift
1 2 3 4 5 6 7 import SwiftUIimport Combinefinal class UserData : ObservableObject { var showFavoritesOnly = false var landmarks = landmarkData }
ObservableObject
需要发布对其数据的任何更改,以便其订阅者可以获取更改。
步骤4
将@Published
属性添加到模型中的每个属性
UserData.swift
1 2 3 4 5 6 7 import SwiftUIimport Combinefinal class UserData : ObservableObject { @Published var showFavoritesOnly = false @Published var landmarks = landmarkData }
第五节 在视图中采用你的模型对象
现在您已经创建了UserData
对象,您需要更新视图以将其作为应用程序的数据存储。
步骤1
在LandmarkList.swift
中,用@EnvironmentObject
属性替换showFavoritesOnly
声明,并向预览添加environmentObject(:)
修饰符。
只要environmentObject(:)
修饰符已应用于父对象,此userData
属性就会自动获取其值。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import SwiftUIstruct LandmarkList : View { @EnvironmentObject var userData: UserData var body: some View { NavigationView { List { Toggle (isOn: $showFavoritesOnly ) { Text ("Favorites only" ) } ForEach (landmarkData) { landmark in if ! self .showFavoritesOnly || landmark.isFavorite { NavigationLink (destination: LandmarkDetail (landmark: landmark)) { LandmarkRow (landmark: landmark) } } } } .navigationBarTitle(Text ("Landmarks" )) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () .environmentObject(UserData ()) } }
步骤2
通过访问userData
上的相同属性来替换showFavoritesOnly
的使用。
与@State
属性一样,您可以使用$
访问到userData
对象成员的绑定。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import SwiftUIstruct LandmarkList : View { @EnvironmentObject var userData: UserData var body: some View { NavigationView { List { Toggle (isOn: $userData .showFavoritesOnly) { Text ("Favorites only" ) } ForEach (landmarkData) { landmark in if ! self .userData.showFavoritesOnly || landmark.isFavorite { NavigationLink (destination: LandmarkDetail (landmark: landmark)) { LandmarkRow (landmark: landmark) } } } } .navigationBarTitle(Text ("Landmarks" )) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () .environmentObject(UserData ()) } }
步骤3
创建ForEach
实例时使用userData.landmarks
作为数据。
LandmarkList.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import SwiftUIstruct LandmarkList : View { @EnvironmentObject var userData: UserData var body: some View { NavigationView { List { Toggle (isOn: $userData .showFavoritesOnly) { Text ("Favorites only" ) } ForEach (userData.landmarks) { landmark in if ! self .userData.showFavoritesOnly || landmark.isFavorite { NavigationLink (destination: LandmarkDetail (landmark: landmark)) { LandmarkRow (landmark: landmark) } } } } .navigationBarTitle(Text ("Landmarks" )) } } } struct LandmarkList_Previews : PreviewProvider { static var previews: some View { LandmarkList () .environmentObject(UserData ()) } }
步骤4
在SceneDelegate.swift
中,将environmentObject(:)
修饰符添加到LandmarkList
。
如果您在模拟器或设备上构建并运行地标,而不是使用预览,则此更新将确保地标列表在环境中有一个UserData
对象。
SceneDelegate.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import UIKitimport SwiftUIclass SceneDelegate : UIResponder , UIWindowSceneDelegate { var window: UIWindow ? func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene .ConnectionOptions ) { if let windowScene = scene as? UIWindowScene { let window = UIWindow (windowScene: windowScene) window.rootViewController = UIHostingController ( rootView: LandmarkList () .environmentObject(UserData ()) ) self .window = window window.makeKeyAndVisible() } } }
步骤5
更新LandmarkDetail
视图以在环境中使用UserData
对象。
在访问或更新地标的收藏状态时,您将使用地标索引,以便始终访问该数据的正确版本。
LandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import SwiftUIstruct LandmarkDetail : View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0 .id == landmark.id })! } var body: some View { VStack { MapView (landmark: landmark) .frame(height: 300 ) CircleImage (image: landmark.image) .offset(y: - 130 ) .padding(.bottom, - 130 ) VStack (alignment: .leading) { Text (landmark.name) .font(.title) HStack (alignment: .top) { Text (landmark.park) .font(caption) Spacer () Text (landmark.state) .font(.caption) } } .padding() Spacer () } .navigationBarTitle(Text (landmark.name), displayMode: .inline) } } struct LandmarkDetail_Preview : PreviewProvider { static var previews: some View { LandmarkDetail (landmark: landmarkData[0 ]) .environmentObject(UserData ()) } }
步骤6
切换回LandmarkList.swift
并打开实时预览,以验证一切正常工作。
第六节 为每个地标创建收藏夹按钮
地标应用程序现在可以在过滤和未经过滤的地标视图之间切换,但最喜欢的地标列表仍然是硬编码的。要允许用户添加和删除收藏,需要将收藏按钮添加到地标详情视图。
步骤1
在LandmarkDetail.swift
中,将地标的名称嵌入HStack
。
LandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import SwiftUIstruct LandmarkDetail : View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0 .id == landmark.id })! } var body: some View { VStack { MapView (landmark: landmark) .frame(height: 300 ) CircleImage (image: landmark.image) .offset(y: - 130 ) .padding(.bottom, - 130 ) VStack (alignment: .leading) { HStack { Text (landmark.name) .font(.title) } HStack (alignment: .top) { Text (landmark.park) .font(caption) Spacer () Text (landmark.state) .font(.caption) } } .padding() Spacer () } .navigationBarTitle(Text (landmark.name), displayMode: .inline) } } struct LandmarkDetail_Preview : PreviewProvider { static var previews: some View { LandmarkDetail (landmark: landmarkData[0 ]) .environmentObject(UserData ()) } }
步骤2
在地标名称旁边创建一个新按钮。使用if-else
条件语句提供不同的图像,以指示地标是否收藏。
在按钮的action
闭包中,代码使用带有userData
对象的landmarkIndex
来更新地标。
LandmarkDetail.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import SwiftUIstruct LandmarkDetail : View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0 .id == landmark.id })! } var body: some View { VStack { MapView (landmark: landmark) .frame(height: 300 ) CircleImage (image: landmark.image) .offset(y: - 130 ) .padding(.bottom, - 130 ) VStack (alignment: .leading) { HStack { Text (landmark.name) .font(.title) Button (action: { self .userData.landmarks[self .landmarkIndex].isFavorite.toggle() }) { if self .userData.landmarks[self .landmarkIndex].isFavorite { Image (systemName: "star.fill" ) .foregroundColor(Color .yellow) } else { Image (systemName: "star" ) .foregroundColor(Color .gray) } } } HStack (alignment: .top) { Text (landmark.park) .font(caption) Spacer () Text (landmark.state) .font(.caption) } } .padding() Spacer () } .navigationBarTitle(Text (landmark.name), displayMode: .inline) } } struct LandmarkDetail_Preview : PreviewProvider { static var previews: some View { LandmarkDetail (landmark: landmarkData[0 ]) .environmentObject(UserData ()) } }
步骤3
切换回LandmarkList.swift
,并打开实时预览。
当您从列表导航到详情信息并点击按钮时,这些更改将在您返回列表时保持不变。因为两个视图都在访问环境中的同一个模型对象,所以两个视图保持一致性。
User Interface
视图和控件
在屏幕上显示内容并处理用户交互。
概述
视图和控件是应用程序用户界面的可视化构建区块。使用它们在屏幕上显示应用程序的内容。视图可以描述文本、图像、形状、自定义绘图以及所有这些内容的组合。控件允许用户使用一致的API与其相应的平台和上下文进行交互。
使用指定其视觉关系和层次结构的容器合并视图。使用名为修饰符modifiers
的方法自定义内置视图和为应用程序创建的视图的显示、行为和交互。
将修饰符modifiers
应用于视图和控件:
控制视图的大小、位置和外观属性。
响应轻触、手势和其他用户交互。
支持拖拽操作。
自定义动画和转场。
设置样式首选项和其他环境数据。
有关如何使用视图和控件的其他信息,请参见人机界面指南。
话题
摘要
protocol View
视图:用来描述SwiftUI的视图类型。
Creating and Combining Views
创建并组合视图:本教程将指导您构建地标,这是一个iOS应用程序,用于发现和共享您喜欢的地方。您将首先构建显示地标的详情视图。
Working with UI Controls
使用UI控件:在地标应用程序中,用户可以创建个人简介来表达他们的个性。为了让用户能够更改他们的个人简介,您将添加一个编辑模式并设计首选项页面。
文本 Text
struct Text
文本:显示一行或多行只读文本的视图。
struct TextField
文本输入框:显示可编辑文本的控件。
struct SecureField
密文输入框:用户安全输入私密文本的控件。
struct Font
字体:依赖于环境的字体。
图像 Images
struct Image
图像:显示依赖于环境的图像视图。
struct Button
按钮:触控时执行操作的控件。
struct NavigationLink
导航链接:按下时触发导航显示的按钮。
struct MenuButton
菜单按钮:当按下时显示包含选项列表的菜单的按钮。
struct EditButton
编辑按钮:切换当前编辑范围的编辑模式的按钮。
struct PasteButton
粘贴按钮:触发从粘贴板读取数据的系统按钮。
值选择器 Value Selectors
struct Toggle
开关:在打开和关闭状态之间切换的控件。
struct Picker
选择器:从一组互斥值中进行选择的控件。
struct DatePicker
日期选择器:用于选择绝对日期的控件。
struct Slider
滑块:从有界线性值范围中选择值的控件。
struct Stepper
步进器:用于执行递增和递减操作的控件。
支持类型 Supporting Types
struct ViewBuilder
视图构建器:从闭包构造视图的自定义参数属性。
protocol ViewModifier
视图修饰器:应用于视图或其他视图的修饰器,生成原始值的不同版本。
protocol
View
一个表示SwiftUI视图的类型。
代码释义
声明
protocol View
概述
通过声明遵守View protocol
的类型来创建自定义视图。实现所需的body
计算属性,以提供自定义视图的内容和行为。
主题
1 - Implementing a Custom View - 实现自定义视图
1.1 视图的内容和行为。必需的,默认实现。
demo
1 2 3 4 5 6 7 8 9 10 struct ContentView : View { var body: some View { Text ("Hello, World!" ) .background(Color .yellow) .border(Color .red, width: 2 ) } }
2 - Sizing a Views - 制定视图的大小
2.1 将视图定位在具有指定大小的不可见框架中
1 func frame (width : CGFloat ?, height : CGFloat ?, alignment : Alignment ) -> View
2.2 将视图定位在具有指定宽度和高度的不可见框架中。
1 func frame (minWidth : CGFloat ?, idealWidth : CGFloat ?, maxWidth : CGFloat ?, minHeight : CGFloat ?, idealHeight : CGFloat ?, maxHeight : CGFloat ?, alignment : Alignment ) -> View
2.3 将视图修复为理想大小。
2.4 将视图修复为理想大小,可指定垂直或水平方向的修复。
1 func fixedSize (horizontal : Bool , vertical : Bool ) -> View
2.5 设置父布局应将空间分配给子布局的优先级,默认为0。
1 func layoutPriority (Double ) -> View
demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 struct SizingInViews : View { var body: some View { VStack { Text ("Hello, World!" ) .background(Color .yellow) .border(Color .red, width: 2 ) .frame(width: 200 , height: 50 , alignment: .topLeading) Text ("Hello, World 2Hello, World 2Hello, World 2Hello, World 2Hello, World 2Hello, World 2Hello" ) .background(Color .yellow) .border(Color .red, width: 2 ) .frame(minWidth: 10 , idealWidth: 60 , maxWidth: 300 , minHeight: 10 , idealHeight: 60 , maxHeight: 300 , alignment: .topTrailing) Text ("Hello, World3" ) .background(Color .yellow) .border(Color .red, width: 2 ) .fixedSize() Text ("Hello, World4Hello, World4HelloHello, World4Hello, World4HelloHello" ) .background(Color .yellow) .border(Color .red, width: 2 ) .fixedSize(horizontal: true , vertical: false ) .frame(width: 200 , height: 200 ) Text ("Hello, World5" ) .background(Color .yellow) .border(Color .red, width: 2 ) .layoutPriority(1 ) } } }
3 - Positioning a View - 定位视图
3.1 将视图的中心固定在其父坐标空间的指定点上。
1 func position (CGPoint ) -> View
3.2 将视图的中心固定在其父坐标空间中指定的坐标上。
1 func position (x : CGFloat , y : CGFloat ) -> View
3.3 通过给定Size
中的width
和height
偏移视图。
1 func offset (CGSize ) -> View
3.4 通过指定的x
和y
值偏移视图。
1 func offset (x : CGFloat , y : CGFloat ) -> View
3.5 将视图延展到指定边缘的安全区域之外。
1 func edgesIgnoringSafeArea (Edge .Set ) -> View
3.6 将名称分配给此视图的坐标空间,此视图的后代可以将其引用。
1 func coordinateSpace <T >(name : T ) -> View
demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 struct PositioningAViews : View { var body: some View { ZStack { Text ("Hello, World!" ) .background(Color .red) .position(CGPoint (x: 100 , y: 0 )) Text ("Hello, World2" ) .background(Color .green) .position(x: 100 , y: 30 ) Text ("Hello, World3" ) .background(Color .red) .position(x: 100 , y: 60 ) .offset(CGSize (width: 30 , height: 0 )) Text ("Hello, World4" ) .background(Color .green) .position(x: 100 , y: 90 ) .offset(x: - 30 , y: 0 ) } .edgesIgnoringSafeArea(.top) .coordinateSpace(name: "test" ) } }
4 - Aligning Views - 设置视图的对其方式
4.1 设置视图的水平对齐方式。
1 func alignmentGuide (HorizontalAlignment , computeValue : (ViewDimensions ) -> CGFloat ) -> View
4.2 设置视图的垂直
对齐方式。
1 func alignmentGuide (VerticalAlignment , computeValue : (ViewDimensions ) -> CGFloat ) -> View
4.3 视图在其自身的坐标空间中的大小和对齐方式的规则。
4.4 用于标识对齐规则的类型。
5 - Adjusting the Padding of a View - 调整视图边距
5.1 将视图沿所有边缘内嵌填充指定的大小。
1 func padding (CGFloat ) -> View
5.2 使用指定的EdgeInsets
内嵌填充视图。
1 func padding (EdgeInsets ) -> View
5.3 使用指定的Edge
集合内嵌填充指定大小。
1 func padding (Edge .Set , CGFloat ?) -> View
5.4 定义了矩形各边的内嵌距离的结构体。
demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct AdjustingPaddingView : View { var body: some View { ZStack { Text ("Hello, World!" ) .background(Color .yellow) .border(Color .red, width: 2 ) .position(x: 100 , y: 30 ) .padding() } .background(Color .gray) .padding([.leading,.trailing], 20 ) } }
留言與分享