App Clips详细解析(三) —— 一个简单示例(二)

版本记录

版本号 时间
V1.0 2020.11.19 星期四

前言

App Clips是2020年WWDC新推出的功能,它的功能非常强大,因为它使没有您的应用程序的用户仍可以使用其功能。 从订购咖啡到停车,App Clips有很多很好的用途。 下面我们就一起学习和看一下。
1. App Clips详细解析(一) —— 基本概览(一)
2. App Clips详细解析(二) —— 一个简单示例(一)

源码

1. Swift

首先看下工程组织结构

下面就是源码啦

1. Package.swift
import PackageDescription

let package = Package(
  name: "LemonadeStandLocations",
  products: [
    .library(
      name: "LemonadeStandLocations",
      targets: ["LemonadeStandLocations"])
  ],
  dependencies: [
  ],
  targets: [
    .target(
      name: "LemonadeStandLocations",
      dependencies: [])
  ]
)
2. LemonadeStand.swift
import MapKit
import SwiftUI
import LemonadeStandLocations

struct LemonadeStand {
  let id = UUID()
  let title: String
  let coordinate: CLLocationCoordinate2D
  var isFavorite: Bool
  let menu: [Lemonade]
}

extension LemonadeStand: Identifiable { }

let standData = [
  LemonadeStand(
    title: "LA Galaxy",
    coordinate: .dignityHealthSportsPark,
    isFavorite: true,
    menu: fullMenu),
  LemonadeStand(
    title: "Chicago Fire",
    coordinate: .soldierField,
    isFavorite: false,
    menu: fullMenu),
  LemonadeStand(
    title: "Seattle Sounders",
    coordinate: .centuryLinkField,
    isFavorite: false,
    menu: fullMenu),
  LemonadeStand(
    title: "New York City FC",
    coordinate: .yankeeStadium,
    isFavorite: false,
    menu: expressMenu),
  LemonadeStand(
    title: "Los Angeles FC",
    coordinate: .bancOfCalifornia,
    isFavorite: false,
    menu: expressMenu)
]
3. Lemonade.swift
import Foundation

struct Lemonade {
  let id = UUID()
  let title: String
  let imageName: String
  let calories: Int
}

extension Lemonade: Identifiable { }

let fullMenu = [
  Lemonade(
    title: "Lemon",
    imageName: "lemon",
    calories: 120),
  Lemonade(
    title: "Lime",
    imageName: "lime",
    calories: 120),
  Lemonade(
    title: "Watermelon",
    imageName: "watermelon",
    calories: 110),
  Lemonade(
    title: "Frozen Lemon",
    imageName: "lemon",
    calories: 140),
  Lemonade(
    title: "Frozen Lime",
    imageName: "lime",
    calories: 140),
  Lemonade(
    title: "Frozen Watermelon",
    imageName: "watermelon",
    calories: 110)
]

let expressMenu = [
  Lemonade(
    title: "Lemon",
    imageName: "lemon",
    calories: 120),
  Lemonade(
    title: "Lime",
    imageName: "lime",
    calories: 120),
  Lemonade(
    title: "Watermelon",
    imageName: "watermelon",
    calories: 110)
]
4. ContentView.swift
import SwiftUI

struct ContentView: View {
  @State private var hideFavorites = false
  @State var stands = standData

  var body: some View {
    StandTabView()
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}
5. StandTabView.swift
import SwiftUI

struct StandTabView: View {
  @State var stands = standData

  var body: some View {
    TabView {
      NavigationView {
        StandList(
          stands: $stands,
          tabTitle: "Stands",
          hideFav: false)
        MenuList(stand: stands[0])
      }
      .tabItem {
        Label("Stands", systemImage: "house")
      }

      NavigationView {
        StandList(
          stands: $stands,
          tabTitle: "Favorite Stands",
          hideFav: true)
        MenuList(stand: stands[0])
      }
      .tabItem {
        Label("Favorites", systemImage: "heart.fill")
      }
    }
  }
}

struct StandTabView_Previews: PreviewProvider {
  static var previews: some View {
    StandTabView()
  }
}
6. MenuList.swift
import SwiftUI

struct MenuList: View {
  let stand: LemonadeStand

  var body: some View {
    List(stand.menu, id: \.id) { item in
      NavigationLink(
        destination: DetailView(lemonade: item)) {
        Text(item.title)
      }
    }.navigationTitle(stand.title)
  }
}

struct MenuList_Previews: PreviewProvider {
  static var previews: some View {
    MenuList(stand: standData[0])
  }
}
7. DetailView.swift
import SwiftUI
import PassKit
struct DetailView: View {
  @State private var orderPlaced = false
  @State private var showWarningAlert = false

  #if APPCLIP
  @EnvironmentObject private var model: SwiftyLemonadeClipModel
  #endif

  let lemonade: Lemonade

  private func placeOrder() {
    #if APPCLIP
    guard model.paymentAllowed else {
      showWarningAlert = true
      return
    }
    #endif

    orderPlaced = true
  }

  var body: some View {
    VStack {
      Image(lemonade.imageName)
        .resizable()
        .frame(maxWidth: 300, maxHeight: 600)
        .aspectRatio(contentMode: .fit)
      Text(lemonade.title)
        .font(.headline)
      Divider()
      Text("\(lemonade.calories) Calories")
        .font(.subheadline)
        .padding(15)
      // swiftlint:disable:next multiple_closures_with_trailing_closure
      Button(action: { placeOrder() }) {
        Text("Place Order")
          .foregroundColor(.white)
      }
      .frame(minWidth: 100, maxWidth: 400)
      .frame(height: 45)
      .background(Color.black)
    }
    .padding()
    .navigationBarTitle(Text(lemonade.title), displayMode: .inline)
    .sheet(isPresented: $orderPlaced, onDismiss: nil) {
      OrderPlacedView(lemonade: lemonade)
    }
    .alert(isPresented: $showWarningAlert) {
      Alert(
        title: Text("Payment Disabled"),
        message: Text("The QR was scanned at an invalid location."),
        dismissButton: .default(Text("OK"))
      )
    }
  }
}

struct DetailView_Previews: PreviewProvider {
  static var previews: some View {
    DetailView(lemonade: standData[0].menu[0])
  }
}
8. OrderPlacedView.swift
import SwiftUI

struct OrderPlacedView: View {
  let lemonade: Lemonade

  var body: some View {
    VStack {
      Image(lemonade.imageName)
        .resizable()
        .frame(maxWidth: 300, maxHeight: 600)
        .aspectRatio(contentMode: .fit)
      Text("Thank you!\nYour Lemonade is on its way!")
        .lineLimit(2)
        .multilineTextAlignment(.center)
        .padding(15)
    }
  }
}

struct OrderPlacedView_Previews: PreviewProvider {
  static var previews: some View {
    OrderPlacedView(lemonade: standData[0].menu[0])
  }
}
9. StandList.swift
import SwiftUI

struct StandList: View {
  @Binding var stands: [LemonadeStand]
  let tabTitle: String
  let hideFav: Bool

  var showFav: [LemonadeStand] {
    hideFav ? stands.filter { $0.isFavorite == true } : stands
  }

  private func setFavorite(_ favorite: Bool, for stand: LemonadeStand) {
    if let index = stands.firstIndex(
      where: { $0.id == stand.id }) {
      stands[index].isFavorite = favorite
    }
  }

  var body: some View {
    List(showFav) { stand in
      NavigationLink(
        destination: MenuList(stand: stand)) {
        Label(
          "\(stand.title)",
          systemImage: stand.isFavorite ? "heart.fill": "heart"
        )
        .contextMenu {
          Button("Like") {
            setFavorite(true, for: stand)
          }
          Button("Unlike") {
            setFavorite(false, for: stand)
          }
        }
      }
    }
    .navigationBarTitle(tabTitle)
  }
}
10. AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { }
11. SceneDelegate.swift
import UIKit
import SwiftUI

class 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: ContentView())
      self.window = window
      window.makeKeyAndVisible()
    }
  }
}
12. SwiftyLemonadeClipApp.swift
import SwiftUI
import MapKit
import AppClip
import CoreLocation

@main
struct SwiftyLemonadeClipApp: App {
  @StateObject private var model = SwiftyLemonadeClipModel()
  private var locationManager = CLLocationManager()

  var body: some Scene {
    WindowGroup {
      ContentView()
        .environmentObject(model)
        .onContinueUserActivity(
          NSUserActivityTypeBrowsingWeb,
          perform: handleUserActivity)
        .onAppear {
          requestNotificationAuthorization()
        }
    }
  }

  func handleUserActivity(_ userActivity: NSUserActivity) {
    guard
      let incomingURL = userActivity.webpageURL,
      let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
      let queryItems = components.queryItems
    else {
      return
    }
    guard
      let latValue = queryItems.first(where: { $0.name == "lat" })?.value,
      let lonValue = queryItems.first(where: { $0.name == "lon" })?.value,
      let lat = Double(latValue),
      let lon = Double(lonValue)
    else {
      return
    }

    let location = CLLocationCoordinate2D(
      latitude: CLLocationDegrees(lat),
      longitude: CLLocationDegrees(lon))

    if let stand = standData.first(where: { $0.coordinate == location }) {
      model.selectedStand = stand
    } else {
      model.locationFound = false
    }

    guard let payload = userActivity.appClipActivationPayload else {
      return
    }

    let region = CLCircularRegion(
      center: location,
      radius: 500,
      identifier: "stand_location"
    )

    payload.confirmAcquired(in: region) { inRegion, error in
      guard error == nil else {
        print(String(describing: error?.localizedDescription))
        return
      }
      DispatchQueue.main.async {
        model.paymentAllowed = inRegion
      }
    }
  }


  func requestNotificationAuthorization() {
    let notifCenter = UNUserNotificationCenter.current()
    notifCenter.getNotificationSettings { setting in
      if setting.authorizationStatus == .ephemeral {
        return
      }
      notifCenter.requestAuthorization(options: .alert) { result, error  in
        print("Authorization Request result: \(result) - \(String(describing: error))")
      }
    }
  }
}
13. ContentView.swift
import SwiftUI

struct ContentView: View {
  @EnvironmentObject private var model: SwiftyLemonadeClipModel

  var body: some View {
    if let selectedStand = model.selectedStand {
      NavigationView {
        MenuList(stand: selectedStand)
      }
    }
    if model.locationFound == false {
      Text("Error finding stand.")
    }
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}
14. SwiftyLemonadeClipModel.swift
import Foundation
class SwiftyLemonadeClipModel: ObservableObject {
  @Published var selectedStand: LemonadeStand?
  @Published var paymentAllowed = true
  @Published var locationFound = true
}

后记

本篇主要讲述了App Clips的一个简单示例,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容