SwiftUI官方教程解讀
原文鏈接:https://www.jianshu.com/p/ecfdbea7a0ed
SwiftUI是wwdc2019發布的一個新的UI框架,通過聲明和修改視圖來布局UI和創建流暢的動畫效果。并且我們可以通過狀態變量來進行數據綁定實現一次性布局;Xcode 11 內建了直觀的新設計工具canvus,在整個開發過程中,預覽可視化與代碼可編輯性能同時支持并交互,讓我們可以體驗到代碼和布局同步的樂趣;同時支持和UIkit的交互
- 開發者可以在canvus中拖拽控件來構建界面, 所編輯的內容會立刻反應到代碼上
- 切換不同的視圖文件時canvus會切換到不同的界面
- 點擊左下角的按鈕釘我們可以把視圖固定在活躍頁面
- 選中canvus中的控件command+click可以調出inspect布局控件的屬性
- 點擊右上角的+可以獲取新的控件并拖拽到對應的位置
- 在live狀態下我們可以在canvus中調試點擊等可交互效果 但不能縮放視圖大小
每次修改或者增加屬性需要點擊resume刷新canvus
創建一個SwiftUI文件,默認生成兩個結構體。一個實現view的協議,在body屬性里描述內容和布局;一個結構體聲明預覽的view
并進行初始化等信息,預覽view是控制器的view時可以顯示在多個模擬器設備,是控件view時可以設置frame,預覽view是提供給canvus展示的
,使用了#if DEBUG 指令,編譯器會刪除代碼,不會隨應用程序一起發布
#if DEBUG
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)
//.previewLayout(.fixed(width: 300, height: 70)) 設置view控件大小
}
.environmentObject(UserData())
}
}
#endif
普通的view:將多個視圖組合并嵌入到堆棧中,這些堆棧將視圖水平、垂直或者前后組合在一起
VStack { //這里的布局實現的是上圖canvus中landMarkDetail的效果
MapView(coordinate: landmark.locationCoordinate)
.frame(height: 300)//不傳width默認長度為整個界面
CircleImage(image: landmark.image(forSize: 250))
.offset(x: 0, 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()
}
列表的布局:要求數據是可被標識的
(1)唯一標識每個元素的主鍵路徑
List(landmarkData.identified(by: \.id)) { landmark in
LandmarkRow(landmark: landmark)
}
(2)數據類型實現Identifiable protocol,持有一個id 屬性
struct 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
}
List(landmarkData) { landmark in
LandmarkRow(landmark: landmark)
} //直接傳數據源
添加導航欄是將其嵌入到NavigationView中,點擊跳轉的控件包裝在navigationButton中,以設置到目標視圖的換位。navigationBarTitle設置導航欄的標題,navigationBarItems設置導航欄右邊的item
NavigationView {//顯示導航view
List {
//SwiftUI里面的類似switch的控件,可以在list中直接組合布局
Toggle(isOn: $showFavoritesOnly) {
Text("Favorites only")
}
ForEach(landmarkData) { landmark in
if !self.showFavoritesOnly || landmark.isFavorite {
//跳轉到地標詳細頁面
NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))//導航標題
}
}
實現modal出一個view
.navigationBarItems(trailing:
//點擊navigationBarItems modal出profileHost頁面
PresentationButton(
Image(systemName: "person.crop.circle")
.imageScale(.large)
.accessibility(label: Text("User Profile"))
.padding(),
destination: ProfileHost()
)
)
程序運行是從sceneDelegate定義的根視圖開始的, UIhostingController 是UIViewController的子類
SwiftUI包括帶有預定義或自定義的基本動畫 以及彈簧和流體動畫,可以調整動畫速度,設置延遲,重復動畫等等可以通過在一個動畫修改器后面添加另一個動畫修改器來關閉動畫
- 轉場動畫
系統轉場動畫調用:hikeDetail(hike.hike).transition(.slide)
自定義的轉場動畫:把轉場動畫作為AnyTransition類的類型屬性 (方便點語法設置豐富自定義動畫)
extension 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)
}
}
HikeDetail(hike: hike).transition(.moveAndFade)
調用轉場動畫;move(edge:)方法是讓視圖從同一邊滑出來以及消失;asymmetric(insertion:removal:)設置出現和小時的不同的動畫效果
- 阻尼動畫
var animation: Animation { //定義成存儲屬性方便調用
Animation.spring(initialVelocity: 5)//重力效果,值越大,彈性越大
.speed(2)//動畫時間,值越大動畫速度越快
.delay(0.03 * Double(index))
}
基礎動畫
Button(action: //點擊按鈕顯示一個view帶轉場的動畫效果
withAnimation {
self.showDetail.toggle()
}
}) {
Image(systemName: "chevron.right.circle")
.imageScale(.large)
//旋轉90度
.rotationEffect(.degrees(showDetail ? 90 : 0))
//.animation(nil) //關閉前面的旋轉90度的動畫效果,只顯示下面的動畫
//選中的時候放大為原來的1.5倍
.scaleEffect(showDetail ? 1.5 : 1)
.padding()
// .animation(.basic()) 實現簡單的基礎動畫
//.animation(.spring()) 阻尼動畫
}
給圖片按鈕加動畫效果, 對應的會有旋轉和縮放會有動畫;加到action時,即使點擊完成后的顯示沒有給image的可做動畫屬性加動畫效果,全部都有動畫,包含旋轉縮放和轉場動畫
利用SwiftUI環境中的存儲 ,把自定義數據對象綁定到view ,SwiftUI監視到可綁對象任何影響視圖的更改并在更改后顯示正確的視圖
- 自定義綁定類型
聲明為綁定類型 BindableObject ,PassthroughSubject是Combine框架的消息發布者, SwiftUI通過這個消息發布者訂閱對象,并在數據發生變化的時候更新任何需要刷新的視圖
import Combine
import SwiftUI
final class UserData: BindableObject {
let didChange = PassthroughSubject<UserData, Never>()
var showFavoritesOnly = false {
didSet {
didChange.send(self)
}
}
var landmarks = landmarkData {
didSet {
didChange.send(self)
}
}
}
當客戶機需要更新數據的時候,可綁定對象通知其訂閱者
eg:當其中一個屬性發生更改時,在屬性的didset里面通過didchange發布者發布更改
綁定屬性
(1)state
@State var profile = Profile.default
狀態是隨時間變化影響頁面布局內容和行為的值
給定類型的持久值,視圖通過該持久值讀取和監視該值。狀態實例不是值本身;它是讀取和修改值的一種方法。若要訪問狀態的基礎值,請使用其值屬性。
(2)binding
@Binding var profile: Profile//向子視圖傳遞數據
(3)environmentObject :
@EnvironmentObject var userData: UserData
存儲在當前環境中的數據,跨視圖傳遞
,在初始化持有對象的時候使用environmentObject(_:)賦值可以和前面的自定義綁定類型一起使用
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIHostingController(rootView: CategoryHome().environmentObject(UserData()))
綁定行為
是對可變狀態或數據的引用,用$的前綴訪問狀態變量或者其屬性之一實現綁定控件 也可以訪問綁定屬性來實現綁定
表示UIkit的view和controller 需要創建遵UIViewRepresentable或者UIViewControllerRepresentable協議
的結構體,SwiftUI管理他們的生命周期并在需要的時候更新
實現協議方法:
//創建展示的UIViewController,調用一次
func makeUIViewController(context: Self.Context) -> Self.UIViewControllerType
//將展示的UIViewController更新到最新的版本
func updateUIViewController(_ uiViewController: Self.UIViewControllerType, context: Self.Context)
//創建協調器
func makeCoordinator() -> Self.Coordinator
在結構體內嵌套定義一個coordinator類。SwiftUI管理coordinator并把它提供給context ,在makeUIView(context:)之前調用這個makeCoordinator()方法創建協調器
,以便在配置視圖控制器的時候可以訪問coordinator對象
我們可以使用這個協調器來實現常見的Cocoa模式,例如委托、數據源和通過目標操作響應用戶事件
。
這里以用UIPageViewController實現輪播圖為例,要注意其中的更新頁面的邏輯~
pageview作為主view,組合一個PageControl 和 PageViewController實現圖片輪播效果
PageView: @State var currentPage = 1
定義綁定屬性 ,$currentPage
實現綁定到PageViewController
PageViewController: @Binding var currentPage: Int
定義綁定屬性,在更新的方法updateUIViewController
里面綁定顯示,點擊pagecontrol的更新頁面時pageviewcontroller可以更新到最新的頁面
pagecontrol: @Binding var currentPage: Int
定義綁定屬性 ,updateUIView
綁定顯示,pageview滑動更新頁面 pagecontrol可以更新到正確的顯示
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
@State var currentPage = 1
init(_ views: [Page]) {//傳入的view用SwiftUI的controller包裝好后面傳給pagecontroller
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
ZStack(alignment: .bottomTrailing) {//將currentpage綁定起來了
PageViewController(controllers: viewControllers, currentPage: $currentPage)
PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
.padding()
//Text("Current Page: \(currentPage)").padding(.trailing,30)
}
}
}
import SwiftUI
import UIKit
struct 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綁定currentpage顯示當前的頁面,pageView變化的時候,page更新頁面
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) {
//當view滑動停止的時候告訴pageview當前頁面的index(數據變化 pageview更新pagecontrol的展示)
parent.currentPage = index
}
}
}
}
struct 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
}
}
}
KaTeX parse error: Invalid color: 'rgb(150,90,150)' at position 8: \color{r?g?b?(?1?5?0?,?9?0?,?1?5?0?)?}{QA}: 當我們編輯一部分用戶數據的時候,我們不希望在編輯數據完成的時候影響到其他的頁面 那么我們需要創建一個副本數據, 當副本數據編輯完成的時候 用副本數據更新真正的數據, 使相關的頁面變化 這部分的內容參見demo中profiles的部分;對于畫圖的部分demo中也有非常酷炫的示例,詳情參見 HikeGraph
、Badge
(徽章)
Apple官網教程 :https://developer.apple.com/tutorials/swiftui/creating-and-combining-views
demo下載
SwiftUI documentation
就職于甜橙金融(翼支付)信息技術部,負責 iOS 客戶端開發
轉載于 https://www.jianshu.com/p/ecfdbea7a0ed
[譯] SwiftUI 官方教程 (十)
https://www.jianshu.com/p/17fc7929fcbb
2019.10.16 20:11:14字數 3,511閱讀 311
由于 API 變動,此文章部分內容已失效,最新完整中文教程及代碼請查看 https://github.com/WillieWangWei/SwiftUI-Tutorials
image
微信技術群
SwiftUI
代表未來構建 App 的方向,歡迎加群一起交流技術,解決問題。
加群現在需要申請了,可以先加我微信,備注 "SwiftUI",我會拉你進群。
創建 watchOS App
本教程為你提供一個將你已經學到的關于
SwiftUI
的知識應用到自己的產品上的機會,并且不費吹灰之力就可以將 Landmarks app 遷移到 watchOS 上。首先,給項目添加一個 watchOS target,然后復制為 iOS app 中創建的共享數據和視圖。當所有資源都準備好后,你就可以通過自定義 SwiftUI 視圖,在 watchOS 上顯示詳細信息和列表視圖。
下載項目文件并按照以下步驟操作,也可以打開已完成的項目自行瀏覽代碼。
- 預計完成時間:25 分鐘
- 項目文件:下載
1. 添加一個 watchOS Target
要創建 watchOS app,首先要給項目添加一個 watchOS target。
Xcode 會將 watchOS app 的組和文件,以及構建和運行 app 所需的 scheme 添加到項目中。
image
1.1 選擇 File
> New
> Target
,當模版表單顯示后,選擇 watchOS
標簽,選擇 Watch App for iOS App
模版后點擊 Next
。
這個模版會給項目添加一個新的 watchOS app,將 iOS app 與它配對。
1.2 在表單的 Product Name
中輸入 WatchLandmarks
,將 Language
設置成 Swift
,將 User Interface
設置成 SwiftUI
。勾選 Include Notification Scene
復選框,然后點擊 Finish
。
image
1.3 Xcode 彈出提示,點擊 Activate
。
這樣選擇 WatchLandmarks
scheme 后,就可以構建和運行你的 watchOS app 了。
image
Whenever possible, create an independent watchOS app. Independent watchOS apps don’t require an iOS companion app.
1.4 在 WatchLandmarks Extension
的 General
標簽中,勾選 Supports Running Without iOS App Installation
復選框。
盡可能創建一個獨立的 watchOS app。獨立的 watchOS app 不需要與 iOS app 配套使用。
image
2. 在多個 Target 中共享文件
設置了 watchOS target 后,你需要從 iOS target 中共享一些資源。比如重用 Landmark app 中的數據模型,一些資源文件,以及任何不需要修改就可以跨平臺顯示的視圖。
image
2.1 在項目導航器中,按住 Command
鍵然后點擊選中以下文件:LandmarkRow.swift
, Landmark.swift
, UserData.swift
, Data.swift
, Profile.swift
, Hike.swift
, CircleImage.swift
。
Landmark.swift
, UserData.swift
, Data.swift
, Profile.swift
, Hike.swift
定義了 app 的數據模型。雖然你不會用到所有這些模型,但是需要保證這些文件都編譯到了 app 中。 LandmarkRow.swift
和 CircleImage.swift
是兩個不修改就可以顯示在 watchOS 中的視圖。
2.2 打開 File
檢查器,勾選 Target Membership
中的 WatchLandmarks Extension
復選框。
這會讓你在上一步中選擇的文件在 watchOS app 中可用。
2.3 打開項目導航器,在 Landmark
組中選擇 Assets.xcassets
,然后在 File
檢查器的 Target Membership
中將它添加到 WatchLandmarks
target。
這與你上一步選擇到 target 不一樣, WatchLandmarks Extension
target 包含你的 app 的代碼,而 WatchLandmarks
target 則管理你的故事板,圖標和相關資源。
image
2.4 在項目導航器中,選擇 Resources
文件夾中的所有文件,然后在 File
檢查器的 Target Membership
中將它們添加到 WatchLandmarks Extension
target。
3. 創建詳情視圖
現在 iOS target 的資源在 watch app 上已經可用了,你需要創建一個 watch 獨有的視圖來顯示地標詳情。為了測試這個視圖,你需要給最大和最小 watch 尺寸創建自定義預覽,然后給圓形視圖做一些修改來適配 watch 顯示。
image
3.1 在項目導航器中,單擊 WatchLandmarks Extension
文件夾旁邊的顯示三角形來顯示其內容,然后添加一個新 SwiftUI
視圖,命名為 WatchLandmarkDetail
。
image
3.2 給 WatchLandmarkDetail
結構體添加 userData
, landmark
和 landmarkIndex
屬性。
這些和你在 處理用戶輸入 中添加到 LandmarkDetail
結構體中的屬性是一樣的。
WatchLandmarkDetail.swift
import SwiftUI
struct 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.3 在預覽中,創建一個用戶數據的實例,然后用它給 WatchLandmarkView
結構體的初始化傳遞一個地標對象。另外還需要將這個用戶數據設置成視圖的環境對象。
WatchLandmarkDetail.swift
import SwiftUI
struct 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)
//
}
}
image
3.4 在 WatchLandmarkDetail.swift
中,從 body()
方法里返回一個 CircleImage
視圖。
這就是你從 iOS 項目中復用 CircleImage
視圖的地方。因為創建了可調整大小的圖片, .scaledToFill()
的調用會讓圓形的尺寸自動適配顯示。
WatchLandmarkDetail.swift
import SwiftUI
struct 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)
}
}
image
3.5 給最大 (44mm) 和最小 (38mm) 表盤創建預覽。
通過針對最大和最小表盤的測試,你可以看到你的 app 是如何縮放來適配顯示的。與往常一樣,你應該在所有支持的設備尺寸上測試用戶界面。
WatchLandmarkDetail.swift
import SwiftUI
struct 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")
}
//
}
}
image
圓形圖片重新調整大小來適配顯示的高度。但不幸,這依然裁剪了圓形的寬度。為了修復這個裁剪問題,你需要把圖片嵌入到一個 VStack
中,并且做一些額外的布局修改來讓圓形圖片適配任何 watch 的寬度。
3.6 把圖片嵌入到一個 VStack
中,在圖片下面顯示地標的名字和它的信息。
如你所見,信息并沒有完全適配 watch 的屏幕,但是你可以通過將這個 VStack
放在一個滾動視圖中來修復這個問題。
WatchLandmarkDetail.swift
import SwiftUI
struct 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")
}
}
}
image
3.7 將豎直 stack 包裝中一個滾動視圖中。
這讓視圖可以滾動,但是帶來了另外一個問題:圓形圖片展開到了全屏,并且調整了其他 UI 元素來匹配這個圖片。你需要調整這個圓形圖片的大小來讓它和地標名字顯示在屏幕上。
WatchLandmarkDetail.swift
import SwiftUI
struct 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")
}
}
}
image
3.8 把 scaleToFill()
改成 scaleToFit()
。
這會讓圓形圖片縮放來匹配顯示的寬度。
WatchLandmarkDetail.swift
import SwiftUI
struct 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")
}
}
}
image
3.9 添加填充使地標名字在圓形圖像下方可見。
WatchLandmarkDetail.swift
import SwiftUI
struct 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")
}
}
}
image
3.10 給返回按鈕添加一個標題。
這里將返回按鈕的文字設置成來 Landmarks
,但是在本教程后面的部分中,只有添加 LandmarksList
視圖后,你才能看到返回按鈕。
WatchLandmarkDetail.swift
import SwiftUI
struct 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")
}
}
}
image
4. 添加 watchOS 地圖視圖
現在你已經創建了基本的詳情視圖,可以添加地圖視圖來顯示地標的位置了。與 CircleImage
不同,你不能僅僅重用 iOS app 的 MapView
。相對的,你需要創建一個 WKInterfaceObjectRepresentable
結構體來包裝 WatchKit 地圖。
image
4.1 給 WatchKit extension
添加一個自定義視圖,命名為 WatchMapView
。
WatchMapView.swift
import SwiftUI
struct WatchMapView: View {
var body: some View {
Text("Hello World!")
}
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
WatchMapView()
}
}
image
4.2 在 WatchMapView
結構體中,將 View
改成 WKInterfaceObjectRepresentable
。
在步驟 1 和 2 所示的代碼之間來回滾動來查看區別。
WatchMapView.swift
import SwiftUI
//
struct WatchMapView: WKInterfaceObjectRepresentable {
//
var body: some View {
Text("Hello World!")
}
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
WatchMapView()
}
}
Xcode 會顯示編譯錯誤,因為 WatchMapView
還沒有遵循 WKInterfaceObjectRepresentable
屬性。
4.3 刪除 body()
方法,將其替換為 landmark
屬性。
每當你創建一個地圖視圖你都需要給這個屬性傳遞一個值。比如,你可以給預覽傳遞一個地標實例。
WatchMapView.swift
import SwiftUI
struct WatchMapView: WKInterfaceObjectRepresentable {
//
var landmark: Landmark
//
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
//
WatchMapView(landmark: UserData().landmarks[0])
//
}
}
4.4 實現 WKInterfaceObjectRepresentable
協議的 makeWKInterfaceObject(context:)
方法。
這個方法會創建 WatchMapView
用來顯示的 WatchKit 地圖。
WatchMapView.swift
import SwiftUI
struct 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])
}
}
4.5 實現 WKInterfaceObjectRepresentable
協議的 updateWKInterfaceObject(_:, context:)
方法,根據地標坐標設置地圖的范圍。
現在項目可以成功構建而沒有任何錯誤了。
WatchMapView.swift
import SwiftUI
struct 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])
}
}
image
4.6 選中 WatchLandmarkView.swift
文件,然后把地圖視圖添加到豎直 stack 的底部。
代碼在地圖視圖之后添加了一個分割線。.scaledToFit()
和 .padding()
修飾符讓地圖的尺寸很好的匹配了屏幕。
WatchLandmarkDetail.swift
import SwiftUI
struct 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")
}
}
}
image
5. 創建一個跨平臺的列表視圖
對于地標列表,你可以重用 iOS app 中的行視圖,但是每個平臺需要展示其自身的詳情視圖。為此,你需要將明確定義詳情視圖的 LandmarkList
視圖轉換為范型列表類型,
image
5.1 在工具欄中,選中 Landmarks scheme
。
Xcode 現在會構建和運行 app 的 iOS 版本。在把列表移動到 watchOS app 之前,你需要確認任何對 LandmarkList
視圖對修改在 iOS app 中依然生效。
5.2 選中 LandmarkList.swift
然后修改類型的聲明將其變成范型類型。
LandmarksList.swift
import SwiftUI
//
struct 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
錯誤。接下來的幾步會修復這些錯誤。
5.3 添加一個創建詳情視圖的閉包屬性。
LandmarksList.swift
import SwiftUI
struct 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())
}
}
5.4 使用 detailViewProducer
屬性給地標創建詳情視圖。
LandmarksList.swift
import SwiftUI
struct 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
的實例后,你還需要提供一個給地標創建詳情視圖的閉包。
5.5 選中 Home.swift
,在 CategoryHome
結構體的 body()
方法中添加一個閉包來創建 LandmarkDetail
視圖。
Xcode 會根據閉包的返回類型來推斷 LandmarkList
結構體的類型。
Home.swift
import SwiftUI
struct 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()
}
}
// swiftlint:disable type_name
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
.environmentObject(UserData())
}
}
5.6 在 LandmarkList.swift
中,給預覽添加類似的代碼。
在這里,你需要使用條件編譯來根據 Xcode 的當前 scheme 來定義詳細視圖。Landmark app 現在可以按預期在 iOS 上構建并運行了。
LandmarksList.swift
import SwiftUI
struct 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())
}
}
6. 添加地標列表
現在你已經更新了 LandmarksList
視圖讓其能在兩個平臺上都工作,可以將它添加到 watchOS app 中了。
image
6.1 在文件檢查器中,把 LandmarksList.swift
添加到 WatchLandmarks Extension
target 中。
你現在可以中你的 watchOS app 的代碼中使用 LandmarkList
視圖了。
6.2 在工具欄中,將 scheme 改為 Watch Landmarks
。
6.3 打開 LandmarkList.swift
的同時,恢復預覽。
現在預覽會顯示 watchOS 的列表視圖。
LandmarksList.swift
import SwiftUI
struct 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())
}
}
image
watchOS app 的根是顯示默認 Hello World!
消息的 ContentView
。
6.4 修改 ContentView
來讓它顯示列表視圖。
ContentView.swift
import SwiftUI
struct 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())
}
}
image
6.5 在模擬器中構建并運行 watchOS app。
通過在列表中滾動地標,點擊視圖的地標詳情,并將其標記為收藏來測試 watchOS app 的行為。點擊返回按鈕回到列表,然后打開 Favorite
開關來查看收藏的地標。
7. 創建自定義通知界面
你的 Landmarks 的 watchOS 版本差不多要完成了。在這最后一節,你需要創建一個通知界面來顯示地標信息,當你在某一個收藏的地標附近時,就會收到這個通知。
注意
這一節只包含當你收到通知后如何顯示,并不描述如何設置或發送通知。
image
7.1 打開 NotificationView.swift
并創建一個視圖來顯示地標的信息,標題和消息。
因為任何通知的值都可以為 nil,所以預覽會顯示通知的兩個版本。第一個僅顯示當沒有數據時的默認值,第二個顯示你提供的標題,信息,和位置。
NotificationView.swift
import SwiftUI
struct 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)
//
}
}
7.2 打開 NotificationController
并添加 landmark
, title
,和 message
屬性。
這些數據存儲發送進來的通知的相關值。
NotificationController.swift
import WatchKit
import SwiftUI
import UserNotifications
class NotificationController: WKUserNotificationHostingController<NotificationView> {
//
var landmark: Landmark?
var title: String?
var message: String?
//
override var body: NotificationView {
NotificationView()
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification) {
// This method is called when a notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
}
}
7.3 更新 body()
方法來使用這些屬性。
此方法會實例化你之前創建的通知視圖。
NotificationController.swift
import WatchKit
import SwiftUI
import UserNotifications
class 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() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification) {
// This method is called when a notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
}
}
7.4 定義 LandmarkIndexKey
。
你需要使用這個鍵從通知中提取額地標的索引。
NotificationController.swift
import WatchKit
import SwiftUI
import UserNotifications
class 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() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification) {
// This method is called when a notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
}
}
7.5 更新 didReceive(_:)
方法從推送中解析數據。
這個方法會更新控制器的屬性。調用這個方法后,系統會使控制器的 body
屬性無效,從而更新導航視圖。然后系統會在 Apple Watch 上顯示通知。
NotificationController.swift
import WatchKit
import SwiftUI
import UserNotifications
class 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() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
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 收到通知后,它會創建通知分類關聯當通知控制器。你需要打開并編輯 app 當故事板來給你當通知控制器設置分類。
7.6 這項目導航器中,選中 Watch Landmarks
文件夾,打開 Interface
故事板。在故事板中選擇指向靜態通知界面控制器的箭頭。
image
7.7 在 Attributes
檢查器中,將 Notification Category
的 Name
設置成 LandmarkNear
。
image
配置測試載荷來使用 LandmarkNear
分類,并傳遞通知控制器期望的數據。
7.8 選擇 PushNotificationPayload.apns
文件,然后更新 title
, body
, category
和 landmarkIndex
屬性。確認將分類設置成了 LandmarkNear
。另外,刪除在教程中任何沒有用到的鍵,比如 subtitle
, WatchKit Simulator Actions
,以及 customKey
。
載荷文件會模擬從服務發來的遠程通知數據。
PushNotificationPayload.apns
{
"aps": {
"alert": {
"body": "You are within 5 miles of Silver Salmon Creek."
"title": "Silver Salmon Creek",
},
"category": "LandmarkNear",
"thread-id": "5280"
},
"landmarkIndex": 1
}
7.9 選擇 Landmarks-Watch (Notification)
scheme,然后構建并運行你的 app。
當你第一次運行通知 scheme,系統會請求發送通知的權限。選擇 Allow
。之后模擬器會顯示一個可滾動的通知,它包括:用于標記 Landmarks app 為發送方的框格,通知視圖以及用于通知操作的按鈕。
image
智能推薦
gateway的官方文檔解讀
之前公司用了springcloud的gateway.被一個伙伴留下了一堆的坑,沒辦法只能從頭梳理. 第一步就是確定架構, gateway+consul+springboot 第二步就是確定一個flag 要解決哪些問題 1.解決灰度負載均衡策略問題:如何配置,支持哪些配置 2.解決ip名單和限流的問題:如何配置,支持哪些配置 3.解決路由重寫的問題: 將msg服務,stats服務和action服務整...
SwiftUI Button 基礎教程
Button 觸發時執行操作的控件 使用教程 您可以通過提供操作和標簽來創建按鈕。操作是一個方法或閉包屬性,當用戶單擊或輕觸按鈕時,它會執行某些操作。標簽是描述按鈕操作的視圖,例如,通過顯示文本(如取消)或圖標(如后退箭頭)。 代碼 iOSButton效果 macOS代碼 tvOS代碼 推薦 基礎文章推薦 《SwiftUI是什么,聽聽大牛們如何說》 經典教程推薦 更新近百篇SwiftUI教程《Sw...
JaCoCo官方教程
1 概念 1.1 任務 - Mission JaCoCo應該為基于Java VM的環境中的代碼覆蓋率分析提供標準技術。重點是提供一個輕量級,靈活且文檔齊全的庫,以與各種構建和開發工具集成。 有幾種適用于Java的開源覆蓋技術。在實現Eclipse插件EclEmma時,觀察到它們都不是真正為集成而設計的。它們中的大多數特別適合特定工具(Ant任務,命令行,IDE插件),并且不提供允許在不同上下文中嵌...
RabbitMQ 官方安裝教程
RabbitMQ 官方安裝教程(使用官方推薦的EPEL倉庫) 一、安裝erlang CentOS 6.x CentOS 7.x 如果上面地址失敗,可以從此處獲取新地址,如下所示 安裝 二、下載rabbitMQ 點擊此處下載對應系統的的安裝包。 例如我的系統是6.x,下載的是rabbitmq-server-3.7.3-1.el6.noarch.rpm 三、安裝rabbitMQ 執行以下兩條命令,安裝...
猜你喜歡
freemarker + ItextRender 根據模板生成PDF文件
1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...
電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!
Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...
requests實現全自動PPT模板
http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...
Linux C系統編程-線程互斥鎖(四)
互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...