SwiftUI 官方教程:SwiftUI Tutorials 仅是几个表现 SwiftUI 简略运用的小 demo 而已,简略易学,循序渐进,先看完能够对 SwiftUI 有一个大概的认知。

十:Creating a macOS App

 Framework Integration – Creating a macOS App 创立一个 macOS App。

 在为 watchOS 创立 Landmarks app 的一个版别后,是时候将目光投向更大的方针了:将 Landmarks 带到 Mac。你将以到目前为止所学的一切内容为根底,完善为 iOS、watchOS 和 macOS 构建 SwiftUI 运用程序的体会。

 首先,你将向 project 增加 macOS target,然后重用之前创立的视图和数据。奠定根底后,你将增加一些针对 macOS 量身定制的新视图,并修正其他视图以更好地跨渠道作业。

 依照过程生成此项目,或下载完成的项目自行探究。

Add a macOS Target to the Project

 首先将 macOS target 增加到 project。Xcode 为 macOS app 增加了一组新的 starter files,以及构建和运转 app 所需的 scheme。然后,你将一些现有文件增加到新 target。

 为了能够预览和运转该运用程序,请确保你的 Mac 运转的是 macOS Monterey 或更高版别。

 挑选 File > New > Target,当 template sheet 出现时,挑选 macOS 选项卡,挑选 App template,然后单击下一步。此 template 将新的 macOS app target 增加到 project 中。

 在 sheet 中,输入 MacLandmarks 作为 Product Name。将 interface 设置为 SwiftUI,将 life cycle 设置为 SwiftUI App,将 language 设置为 Swift,然后单击 Finish。

 将 scheme 设置为 MacLandmarks > My Mac。经过将 scheme 设置为 My Mac,你能够预览、构建和运转 macOS app。在学习本教程时,你将运用其他 schemes 来关注其他 targets 怎么呼应共中的更改。

 在 MacLandmarks 文件夹中,挑选 ContentView.swift,翻开 Canvas,然后单击 Resume 以检查 preview。SwiftUI 供给默许主视图及其预览供给程序,就像 iOS 运用程序相同,使你能够预览运用程序的主窗口。

 在 Project navigator 中,从 MacLandmarks 文件夹中删去 MacLandmarksApp.swift 文件;When asked,直接挑选 Move to Trash。

 与 watchOS 运用程序相同,你不需求默许的运用程序结构,由于你将重复运用已有的运用程序结构。

 接下来,你将与 macOS target 共享 iOS app 中的视图、模型和资源文件(view, model, and resource files)。

 在 Project navigator 中,按住 Command 并单击以挑选以下文件:LandmarksApp.swift、LandmarkList.swift、LandmarkRow.swift、CircleImage.swift、MapView.swift 和 FavoriteButton.swift。

 其中第一个是 shared app definition。其他是适用于 macOS 的视图。

 持续按住 Command 单击以挑选 Model 和 Resources 文件夹以及 Asset.xcassets 中的一切项目。

 这些 items 界说运用的 data model and resources。

 在文件检查器中,将 MacLandmarks 增加到所选文件的 Target Membership。

 增加一个 macOS 运用程序图标 set 以匹配其他 targets 的图标 set。

 挑选 MacLandmarks 文件夹中的 Assets.xcasset 文件并删去空的 AppIcon 项。你将在下一步中替换它。

 将 AppIcon.appiconset 文件夹从下载的项目的 Resources 文件夹中拖到 MacLandmark 的 catalog 目录中。

 在 MacLandmarks 文件夹的 ContentView 中,将 LandmarkList 增加为 top level view,并限制 frame 巨细。

 preView 不能正常构建,由于 LandmarkList 运用 LandmarkDetail,但你尚未为 macOS 运用程序界说 detail view。你将在下一节中处理这个问题。

...
struct ContentView: View {
    var body: some View {
        LandmarkList()
            .frame(minWidth: 700, minHeight: 300)
    }
}
...

Create a macOS Detail View

 detail view 显现有关所选 landmark 的信息。你为 iOS 运用创立了这样的视图,但不同的渠道需求不同的数据呈现办法。有时,你能够经过小的调整或条件编译跨渠道重用视图,但 detail view 关于 macOS 来说差异很大,因而最好创立专用视图。你将复制 iOS detail view 作为起点,然后对其进行修正以适应 macOS 的较大显现。

 在 macOS 的 MacLandmarks 文件夹中创立一个名为 LandmarkDetail 的新自界说视图。你现在有三个名为 LandmarkDetail.swift 的文件。每个在视图层次结构中具有相同的用途,但供给针对特定渠道定制的体会。

 将 iOS detail view 内容复制到 macOS detail view 中。预览失利是由于 navigationBarTitleDisplayMode(_:) 办法在 macOS 中不可用。

 删去 navigationBarTitleDisplayMode(_:) modifier,然后将 frame modifier 增加到预览中,以便你能够看到更多内容。

 你将在接下来的几个过程中所做的更改可改善 Mac 更大显现屏的布局。

 将 landmark.park 和 landmark.state 的 Text 所在的 HStack 更改为具有 leading alignment 的 VStack,并移除 Spacer。

...
                VStack(alignment: .leading) {
                    Text(landmark.park)
                    Text(landmark.state)
                }
                .font(.subheadline)
                .foregroundColor(.secondary)
...

 将 MapView 下方的一切内容都包括在 VStack 中,然后将 CircleImage 和 header 的其余部分放在 HStack 中。

 从 CircleImage 中删去 offset,而是对整个 VStack 运用较小的 offset。

 向 image 增加一个 resizable() modifier,并将 CircleImage 限制为更小。

 将 ScrollView 限制为最大宽度。当用户使窗口非常宽时,这会提高可读性。

 更改 FavoriteButton 以运用 plain 按钮款式。在这里运用 plain 款式使按钮看起来更像 iOS equivalent。

 mac 更大的显现屏为你供给更多空间来实现附加功用。

 在 ZStack 中增加 “Open in Maps” 按钮,使其显现在地图右上角的顶部。确保包括 MapKit 以便能够创立你发送到地图的 MKMapItem。

import SwiftUI
import MapKit
struct LandmarkDetail: View {
    @EnvironmentObject var modelData: ModelData
    var landmark: Landmark
    var landmarkIndex: Int {
        modelData.landmarks.firstIndex { $0.id == landmark.id }!
    }
    var body: some View {
        ScrollView {
            ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)) {
                MapView(coordinate: landmark.locationCoordinate)
                    .ignoresSafeArea(edges: .top)
                    .frame(height: 300)
                Button("Open in Maps") {
                    let destination = MKMapItem(placemark: MKPlacemark(coordinate: landmark.locationCoordinate))
                    destination.name = landmark.name
                    destination.openInMaps()
                }
                .padding()
            }
            VStack(alignment: .leading, spacing: 20) {
                HStack(spacing: 24) {
                    CircleImage(image: landmark.image.resizable())
                        .frame(width: 160, height: 160)
                    VStack(alignment: .leading) {
                        HStack {
                            Text(landmark.name)
                                .font(.title)
                            FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
                                .buttonStyle(.plain)
                        }
                        VStack(alignment: .leading) {
                            Text(landmark.park)
                            Text(landmark.state)
                        }
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                    }
                }
                Divider()
                Text("About \(landmark.name)")
                    .font(.title2)
                Text(landmark.description)
            }
            .padding()
            .frame(maxWidth: 700)
            .offset(y: -50)
        }
        .navigationTitle(landmark.name)
    }
}
struct LandmarkDetail_Previews: PreviewProvider {
    static let modelData = ModelData()
    static var previews: some View {
        LandmarkDetail(landmark: modelData.landmarks[0])
            .environmentObject(modelData)
            .frame(width: 850, height: 700)
    }
}

Update the Row View

 shared 的 LandmarkRow 视图在 macOS 中作业,但鉴于新的视觉环境,值得重新审视以寻找改善。由于一切三个渠道都运用此视图,因而你需求注意所做的任何更改都适用于一切渠道。

 在修正 row 之前,请设置列表的预览,由于你将进行的更改取决于行在上下文中的外观。

 翻开 LandmarkList.swift 并增加最小宽度。这改善了预览,但也确保了列表永远不会由于用户调整 macOS 窗口巨细而变得太小。

 固定列表视图预览,以便你能够在进行更改时检查 row 在上下文中的外观。

 翻开 LandmarkRow.swift 并向图画增加 corner radius 以获得更精细的外观。

 将 landmark name 包装在 VStack 中,并将 park 增加为辅佐信息。

struct LandmarkRow: View {
    var landmark: Landmark
    var body: some View {
        HStack {
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
                .cornerRadius(5)
            VStack(alignment: .leading) {
                Text(landmark.name)
                    .bold()
                Text(landmark.park)
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
            Spacer()
            if landmark.isFavorite {
                Image(systemName: "star.fill")
                    .imageScale(.medium)
                    .foregroundColor(.yellow)
            }
        }
    }
}

 在 row 内容周围增加 vertical padding,使每行有更多的喘息空间(breathing room)。

 这些更新改善了 macOS 的外观,但你还需求考虑运用该列表的其他渠道。首先考虑 watchOS。

 挑选 WatchLandmarks target 以检查列表的 watchOS 预览。

 最小行宽在这里不适宜。由于你将在下一节中对列表进行此更改和其他更改,因而最好的处理方案是创立一个省掉宽度约束的特定于手表的列表。

 将一个名为 LandmarkList.swift 的新 SwiftUI 视图增加到名为 WatchLandmarks Extension 文件夹中,该文件夹仅面向 WatchLandmarks Extension,并删去旧文件的 WatchLandmarks Extension target membership。

 将旧 LandmarkList 的内容复制到新 LandmarkList 中,但不运用 frame modifier。content 现在具有正确的宽度,但每一行都有太多信息。

 返回到 LandmarkRow 并增加 #if 条件,以避免 secondary 文本出现在 watchOS 版别中。

 关于 row,运用条件编译是适宜的,由于差异很小。

...
                #if !os(watchOS)
                Text(landmark.park)
                    .font(.caption)
                    .foregroundColor(.secondary)
                #endif
...

 最后,考虑一下你的更改怎么适用于 iOS。

 挑选 Landmarks build target 以检查 iOS 的列表外观。这些更改适用于 iOS,因而无需为该渠道进行任何更新。

Update the List View

 与 LandmarkRow 相同,LandmarkList 现已在 macOS 上运转,但能够运用改善。例如,你能够将仅显现收藏夹的切换开关移至工具栏中的菜单,在该菜单中能够经过其他过滤控件进行衔接。

 你将所做的更改适用于 macOS 和 iOS,但很难在 watchOS 上适应。走运的是,在上一节中,你现已将列表拆分为一个独自的 watchOS 文件。

 返回到 MacLandmarks scheme,在以 iOS 和 macOS 为 target 的 LandmarkList 文件中,在新的 toolbar modifier 中增加一个包括菜单的 ToolbarItem。

 在运转运用之前,你将无法看到 toolbar 更新。将 favorites Toggle 移动到 menu 中。

 有了更多可用空间,你将增加一个用于按 category 过滤地标列表的新控件。

 增加一个 FilterCategory 枚举来描述 filter 状况。将 case strings 与 Landmark structure 中的 Category enumeration 匹配,以便能够比较它们,and include an all case to turn filtering off。

...
    enum FilterCategory: String, CaseIterable, Identifiable {
        case all = "All"
        case lakes = "Lakes"
        case rivers = "Rivers"
        case mountains = "Mountains"
        var id: FilterCategory { self }
    }
...
    @State private var filter = FilterCategory.all

 增加 filter 状况变量,默许为 all case。

 经过在列表视图中存储 filter 状况,用户能够翻开多个列表视图窗口,每个窗口都有自己的 filter 设置,以便能够以不同的方法检查数据。

 更新 filteredLandmarks 以考虑新的 filter 设置,并结合给定 landmark 的类别。

 将 Picker 增加到 menu 以设置 filter 类别。由于 filter 只有几个项,因而运用 inline picker style 使它们一起显现。

...
                    Menu {
                        Picker("Category", selection: $filter) {
                            ForEach(FilterCategory.allCases) { category in
                                Text(category.rawValue).tag(category)
                            }
                        }
                        .pickerStyle(.inline)
                        Toggle(isOn: $showFavoritesOnly) {
                            Text("Favorites only")
                        }
                    } label: {
                        Label("Filter", systemImage: "slider.horizontal.3")
                    }
...

 更新 navigation title 以匹配 filter 的状况。此更改在 iOS 运用中非常有用。

 将第二个子视图增加到 NavigationView,作为 wide layouts 中第二个视图的占位符。增加第二个子视图会自动将列表转换为运用 sidebar list style。

 运转 macOS target 并检查 menu 的运转方法。挑选 Landmarks build target,并运用实时预览检查新的 filtering 是否也适用于 iOS。

Add a Built-in Menu Command

 当你运用 SwiftUI life cycle 创立运用程序时,体系会自动创立一个包括常用 items 的菜单,例如用于关闭最前面的窗口或退出运用程序的 items。 SwiftUI 允许你增加其他具有内置行为的常用 commands,以及完全自界说的 commands。在本节中,你将增加一个体系供给的 command ,该 command 允许用户切换侧边栏,以便在将其拖动关闭后将其取回。

 返回到 MacLandmarks Target,运转 macOS 运用程序,然后将列表和具体视图之间的分隔符一直拖到左边。当你松开鼠标按钮时,列表会消失,无法康复。你将增加一个 command 来处理此问题。

 增加一个名为 LandmarkCommand.swift 的新文件,并将其 Target 设置为同时包括 macOS 和 iOS。你还以 iOS 为 Target,由于共享的 LandmarkList 最终将取决于你在此文件中界说的某些类型。

 Import SwiftUI 并增加符合 Commands 协议的 LandmarkCommands structure,该 structure 具有名为 body 的核算特点。

 与 View structure 相同,Commands structure 需求运用 builder semantics 的 body 的核算特点,但运用 commands 而不是 views 除外。

 将 SidebarCommands command 增加到 body 中。此 built-in command set 包括用于切换边栏的命令。

import SwiftUI
struct LandmarkCommands: Commands {
    var body: some Commands {
        SidebarCommands()
    }
}

 要在 App 中运用 commands,你有必要将它们运用于 scene,接下来将履行此操作。

 翻开 LandmarksApp.swift 文件,然后运用 commands(content:) scene modifier 运用 LandmarkCommands。Scene modifiers 的作业方法与 view modifiers 类似,不同之处在于将它们运用于 scenes 而不是 views。

...
.commands {
    LandmarkCommands()
}
...

 再次运转 macOS App,而且你能够运用 View > Toggle Sidebar 康复列表视图。遗憾的是,watchOS 运用无法构建,由于 Commands 没有 watchOS 可用性。接下来你将修正此问题。

...
#if !os(watchOS)
.commands {
    LandmarkCommands()
}
#endif

 在 commands modifier 周围增加一个条件,以在 watchOS 运用中省掉它。保存后 watchOS App 将再次构建。

Add a Custom Menu Command

 在上一节中,你增加了一个 built-in menu command set。在本节中,你将增加一个 custom command,用于切换当时所选 landmark 的收藏状况。要了解当时挑选了哪个 landmark,你将运用 focused binding。

 在 LandmarkCommands 中,运用名为 SelectedLandmarkKey 的自界说键,运用 selectedLandmark value 扩展 FocusedValues structure。

 界说 focused values 的模式类似于界说 new Environment values 的模式:运用 private key 在 system-defined 的 FocusedValues structure 上读取和写入自界说特点。

import SwiftUI
struct LandmarkCommands: Commands {
    var body: some Commands {
        SidebarCommands()
    }
}
private struct SelectedLandmarkKey: FocusedValueKey {
    typealias Value = Binding<Landmark>
}
extension FocusedValues {
    var selectedLandmark: Binding<Landmark>? {
        get { self[SelectedLandmarkKey.self] }
        set { self[SelectedLandmarkKey.self] = newValue }
    }
}

 增加 @FocusedBinding 特点包装器以跟踪当时选定的 landmark。你正在读取此处的值。稍后将在用户进行挑选的 list view 中进行设置。

@FocusedBinding(\.selectedLandmark) var selectedLandmark

 将一个名为 Landmarks 的新 CommandMenu 增加到 commands 中。接下来,你将界说菜单的内容。

CommandMenu("Landmark") {
}

 向 menu 中增加一个按钮,用于切换所选 landmark 的收藏状况,其外观会根据当时选定的 landmark 及其状况而变化。

CommandMenu("Landmark") {
    Button("\(selectedLandmark?.isFavorite == true ? "Remove" : "Mark") as Favorite") {
        selectedLandmark?.isFavorite.toggle()
    }
    .disabled(selectedLandmark == nil)
}

 运用 keyboardShortcut(_:modifiers:) modifier 为 menu item 增加 keyboard shortcut。SwiftUI 会自动在菜单中显现 keyboard shortcut。

.keyboardShortcut("f", modifiers: [.shift, .option])

 菜单现在包括新 command,但你需求设置 selectedLandmark focused binding 才能使其正常作业。在 LandmarkList.swift 中,为所选 landmark 增加一个状况变量,并增加一个指示所选 landmark 索引的核算特点。

@State private var selectedLandmark: Landmark?
...
var index: Int? {
    modelData.landmarks.firstIndex(where: { $0.id == selectedLandmark?.id } )
}

 运用与 selected value 的 binding 初始化 List,并向 navigation link 增加标记。

List(selection: $selectedLandmark) {
    ...
    .tag(landmark)
}

 增加 focusedValue(_:_:) modifier 到 NavigationView,供给 landmarks array 中的值 binding。

.focusedValue(\.selectedLandmark, $modelData.landmarks[index ?? 0])

 运转 macOS 运用程序并尝试新菜单项。

Add Preferences with a Settings Scene

 用户期望能够运用标准的 Preferences menu item 调整 macOS 运用程序的设置。你将经过增加 Settings scene 来向 MacLandmarks 增加 preferences。scene’s view 界说 preferences 窗口的内容,你将运用该窗口操控 MapView 的初始缩放等级。将值传达给 MapView,并运用 @AppStorage 特点包装器将其永久存储(在本地耐久化)。

 首先,你将在 MapView 中增加一个控件,该控件将初始缩放设置为以下三个等级之一:近、中或远。在 MapView.swift 中,增加缩放枚举以表征缩放等级。

enum Zoom: String, CaseIterable, Identifiable {
    case near = "Near"
    case medium = "Medium"
    case far = "Far"
    var id: Zoom {
        return self
    }
}

 增加一个名为 zoom 的 @AppStorage 特点,该特点默许选用中等缩放等级。运用仅有标识参数的存储键,就像在 UserDefaults 中存储项目时相同,由于这是 SwiftUI 所依赖的底层机制。

@AppStorage("MapView.zoom")
private var zoom: Zoom = .medium

 将用于结构区域特点的经度和纬度增量更改为取决于缩放的值。

var delta: CLLocationDegrees {
    switch zoom {
    case .near: return 0.02
    case .medium: return 0.2
    case .far: return 2
    }
}
...
span: MKCoordinateSpan(latitudeDelta: delta, longitudeDelta: delta)

 若要确保 SwiftUI 在 delta 更改时改写地图,你有必要更改核算和运用 region 的方法。将 region 状况变量、setRegion 办法和地图的 onAppear 修饰符替换为作为 constant binding 传递给地图初始值设定项的 computed region property。

Map(coordinateRegion: .constant(region))
...
var region: MKCoordianteRegion {
    MKCoordinateRegion(
    ...
    )
}

 接下来,你将创立一个操控存储的缩放值的 Settings scene。创立一个名为 LandmarkSettings 的新 SwiftUI 视图,该视图仅面向 macOS 运用。

import SwiftUI
struct LandmarkSettings: View {
    var body: some View {
        Text("Hello, World!")
    }
}
struct LandmarkSettings_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkSettings()
    }
}

 增加一个 @AppStorage 特点,该特点运用与你在 map view 中运用的相同的 key。

@AppStorage("MapView.zoom")
private var zoom: MapView.Zoom = .medium

&emap;增加一个经过 binding 操控缩放值的 Picker。通常运用 Form 在 settings view 中摆放控件。

var body: some View {
    Form {
        Picker("Map Zoom:", selection: $zoom) {
            ForEach(MapView.Zoom.allCases) { level in
                Text(level.rawValue)
            }
        }
        .pickerStyle(.inline)
    }
    .frame(width: 300)
    .navigationTitle("Landmark Settings")
    .padding(80)
}

 在 LandmarksApp.swift 中,将 Settings scene 增加到你的运用程序中,但仅适用于 macOS。

#if os(macOS)
Settings {
    LandmarkSetting()
}
#endif

 运转运用并尝试设置 preferences。请注意,只需你更改缩放等级,地图就会改写。

参阅链接

参阅链接:

  • Creating a macOS App