基于Firebase平台开发(十一) —— Firebase Dynamic Links的简单使用(二)

版本记录

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

前言

Firebase是一家实时后端数据库创业公司,它能帮助开发者很快的写出Web端和移动端的应用。自2014年10月Google收购Firebase以来,用户可以在更方便地使用Firebase的同时,结合Google的云服务。Firebase能让你的App从零到一。也就是说它可以帮助手机以及网页应用的开发者轻松构建App。通过Firebase背后负载的框架就可以简单地开发一个App,无需服务器以及基础设施。接下来几篇我们就一起看一下基于Firebase平台的开发。感兴趣的看下面几篇文章。
1. 基于Firebase平台开发(一) —— 基于ML Kit的iOS图片中文字的识别(一)
2. 基于Firebase平台开发(二) —— 基于ML Kit的iOS图片中文字的识别(二)
3. 基于Firebase平台开发(三) —— Firebase基本使用简介(一)
4. 基于Firebase平台开发(四) —— Firebase基本使用简介(二)
5. 基于Firebase平台开发(五) —— Firebase基本使用简介(三)
6. 基于Firebase平台开发(六) —— 基于Firebase Analytics的App使用率的跟踪(一)
7. 基于Firebase平台开发(七) —— iOS的A/B Test(一)
8. 基于Firebase平台开发(八) —— 使用Firebase Cloud Messaging进行Push Notification的发送和接收(一)
9. 基于Firebase平台开发(九) —— 使用Firebase Cloud Messaging进行Push Notification的发送和接收(二)
10. 基于Firebase平台开发(十) —— Firebase Dynamic Links的简单使用(一)

源码

1. Swift

首先看下工程组织结构

下面就是代码啦

1. AppMain.swift
import SwiftUI
import Firebase

@main
struct AppMain: App {
  // Initialize Firebase
  init() {
    FirebaseApp.configure()
  }

  // Define deepLink
  @State var deepLink: DeepLinkHandler.DeepLink?

  var body: some Scene {
    WindowGroup {
      HomeView()
        .accentColor(Color("rw-green"))
        // Call onOpenURL
        // 1
        .onOpenURL { url in
          print("Incoming URL parameter is: \(url)")
          // 2
          let linkHandled = DynamicLinks.dynamicLinks()
            .handleUniversalLink(url) { dynamicLink, error in
              guard error == nil else {
                fatalError("Error handling the incoming dynamic link.")
              }
              // 3
              if let dynamicLink = dynamicLink {
                // Handle Dynamic Link
                self.handleDynamicLink(dynamicLink)
              }
            }
          // 4
          if linkHandled {
            print("Link Handled")
          } else {
            print("No Link Handled")
          }
        }
        // Add environment modifier
        .environment(\.deepLink, deepLink)
    }
  }

  // MARK: - Functions
  // Handle incoming dynamic link
  func handleDynamicLink(_ dynamicLink: DynamicLink) {
    guard let url = dynamicLink.url else { return }

    print("Your incoming link parameter is \(url.absoluteString)")
    // 1
    guard
      dynamicLink.matchType == .unique ||
      dynamicLink.matchType == .default
    else {
      return
    }
    // 2
    let deepLinkHandler = DeepLinkHandler()
    guard let deepLink = deepLinkHandler.parseComponents(from: url) else {
      return
    }
    self.deepLink = deepLink
    print("Deep link: \(deepLink)")
    // 3
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
      self.deepLink = nil
    }
  }
}
2. HomeView.swift
import SwiftUI

struct HomeView: View {
  var recipes: [Recipe] = Bundle.main.decode("recipe.json")

  @State var cellSelected: Int?

  // Define environment property
  @Environment(\.deepLink) var deepLink

  var body: some View {
    NavigationView {
      ScrollViewReader { proxy in
        ScrollView(.vertical, showsIndicators: false) {
          HStack {
            Text("Raycipe")
              .font(.largeTitle)
              .bold()
            Spacer()
          }
          .padding(.leading)

          VStack(spacing: 20) {
            ForEach(0..<recipes.count) { index in
              NavigationLink(
                destination: RecipeDetailView(recipe: recipes[index]), tag: index, selection: $cellSelected) {
                RecipeCardView(recipe: recipes[index])
                  .padding(.horizontal)
                  .padding(.bottom)
                  .onTapGesture {
                    cellSelected = index
                  }
              }
            }
          }
        }
        // Define navigation
        // 1
        .onChange(of: deepLink) { deepLink in
          guard let deepLink = deepLink else { return }

          switch deepLink {
          case .details(let recipeID):
            // 2
            if let index = recipes.firstIndex(where: {
              $0.recipeID == recipeID
            }) {
              // 3
              proxy.scrollTo(index, anchor: .bottom)
              // 4
              cellSelected = index
            }
          case .home:
            break
          }
        }
      }
    }
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    HomeView()
  }
}
3. RecipeCardView.swift
import SwiftUI

struct RecipeCardView: View {
  var recipe: Recipe

  var body: some View {
    VStack(alignment: .leading, spacing: 5) {
      Image(recipe.image)
        .resizable()
        .scaledToFill()
        .frame(maxWidth: .infinity, maxHeight: 220)
        .clipped()

      VStack(alignment: .leading, spacing: 12) {
        Text(recipe.name)
          .font(.title)
          .fontWeight(.bold)
          .foregroundColor(.black)
        Text(recipe.description)
          .font(.body)
          .foregroundColor(Color.gray)
          .italic()
          .lineLimit(2)
        RecipeInfoView(recipe: recipe)
      }
      .padding()
    }
    .background(Color.white)
    .cornerRadius(12)
    .shadow(color: Color.black.opacity(0.2), radius: 8, x: 0, y: 0)
  }
}

struct RecipeCardView_Previews: PreviewProvider {
  static var previews: some View {
    RecipeCardView(recipe: Recipe.example)
  }
}
4. RecipeDetailView.swift
import SwiftUI
import LinkPresentation
import Firebase

struct RecipeDetailView: View {
  var recipe: Recipe

  let screen = UIScreen.main.bounds
  @State private var isShareSheetShowing = false
  @State var activityIndicator = false

  var body: some View {
    ActivityIndicatorView(isShowing: $activityIndicator) {
      ScrollView(.vertical, showsIndicators: false) {
        VStack(alignment: .center) {
          Image(recipe.image)
            .resizable()
            .scaledToFill()
            .frame(maxWidth: .infinity, maxHeight: 260)
            .clipped()

          Group {
            Text(recipe.description)
              .lineLimit(nil)
              .multilineTextAlignment(.leading)
              .font(.body)
              .padding(.top)
              .frame(minHeight: 100)

            RecipeInfoView(recipe: recipe)

            Button(action: {
              activityIndicator = true
              // Call createDynamicLink
              createDynamicLink()
            }, label: {
              Text("Share")
              Image(systemName: "square.and.arrow.up")
            })
            .font(.title3)
            .foregroundColor(Color("rw-green"))

            Text("Ingredients")
              .font(.title)
              .bold()
              .foregroundColor(Color.primary)
            Text(recipe.ingredients)
              .multilineTextAlignment(.leading)

            Divider()

            Text("Instructions")
              .font(.title)
              .bold()
              .foregroundColor(Color.primary)
            Text(recipe.instructions)
              .lineLimit(nil)
              .multilineTextAlignment(.leading)
          }
          .padding()
        }
      }
      .navigationTitle(Text(recipe.name))
    }
  }

  // MARK: - Functions
  // Create dynamic link
  func createDynamicLink() {
    // TODO 1
    var components = URLComponents()
    components.scheme = "https"
    components.host = "www.raywenderlich.com"
    components.path = "/about"

    // TODO 2
    let itemIDQueryItem = URLQueryItem(name: "recipeID", value: recipe.recipeID)
    components.queryItems = [itemIDQueryItem]

    // TODO 3
    guard let linkParameter = components.url else { return }
    print("I am sharing \(linkParameter.absoluteString)")

    // TODO 4
    let domain = "https://rayciperw.page.link"
    guard let linkBuilder = DynamicLinkComponents.init(link: linkParameter, domainURIPrefix: domain) else {
      return
    }

    // TODO 5
    // 1
    if let myBundleId = Bundle.main.bundleIdentifier {
      linkBuilder.iOSParameters = DynamicLinkIOSParameters(bundleID: myBundleId)
    }
    // 2
    linkBuilder.iOSParameters?.appStoreID = "1481444772"
    // 3
    linkBuilder.socialMetaTagParameters = DynamicLinkSocialMetaTagParameters()
    linkBuilder.socialMetaTagParameters?.title = "\(recipe.name) from Raycipe"
    linkBuilder.socialMetaTagParameters?.descriptionText = recipe.description
    // swiftlint:disable:next force_unwrapping
    linkBuilder.socialMetaTagParameters?.imageURL = URL(string: """
      https://pbs.twimg.com/profile_images/\
      1381909139345969153/tkgxJB3i_400x400.jpg
      """)!

    // TODO 6
    guard let longURL = linkBuilder.url else { return }
    print("The long dynamic link is \(longURL.absoluteString)")

    // TODO 7
    linkBuilder.shorten { url, warnings, error in
      if let error = error {
        print("Oh no! Got an error! \(error)")
        return
      }
      if let warnings = warnings {
        for warning in warnings {
          print("Warning: \(warning)")
        }
      }
      guard let url = url else { return }
      print("I have a short url to share! \(url.absoluteString)")

      shareItem(with: url)
    }
  }

  // Share dynamic link
  func shareItem(with url: URL) {
    activityIndicator = false
    isShareSheetShowing.toggle()
    let subjectLine = "Check out this tasty \(recipe.name) I found on Raycipe!"
    let activityView = UIActivityViewController(activityItems: [subjectLine, url], applicationActivities: nil)
    UIApplication.shared.windows.first?.rootViewController?.present(activityView, animated: true, completion: nil)
  }
}

struct RecipeDetailView_Previews: PreviewProvider {
  static var previews: some View {
    NavigationView {
      RecipeDetailView(recipe: Recipe.example)
    }
  }
}
5. RecipeInfoView.swift
import SwiftUI

struct RecipeInfoView: View {
  // MARK: - PROPERTIES
  var recipe: Recipe

  var body: some View {
    HStack(spacing: 20) {
      Text(recipe.category.localizedUppercase)
        .font(Font.callout.weight(.medium))
        .foregroundColor(Color.white)
        .frame(width: 120, height: 30)
        .background(Color(recipe.category))
        .cornerRadius(12)

      Group {
        HStack(alignment: .center, spacing: 5) {
          Image(systemName: "person.2")
          Text("\(recipe.numberOfServings)")
        }
        HStack(alignment: .center, spacing: 5) {
          Image(systemName: "clock")
          Text(recipe.preparationTime)
        }
      }
      .foregroundColor(.gray)
    }
  }
}

struct RecipeCategoryView_Previews: PreviewProvider {
  static var previews: some View {
    RecipeInfoView(recipe: Recipe.example)
  }
}
6. Recipe.swift
import Foundation

struct Recipe: Codable, Equatable {
  let recipeID: String
  let name: String
  let image: String
  let category: String
  let preparationTime: String
  let numberOfServings: Int
  let description: String
  let ingredients: String
  let instructions: String

  enum CodingKeys: String, CodingKey {
    case recipeID = "recipe-id"
    case name, image, category
    case preparationTime = "preparation-time"
    case numberOfServings = "number-of-servings"
    case description, ingredients, instructions
  }

  #if DEBUG
  static let example = Recipe(
    recipeID: "003",
    name: "Chocolate Cookies",
    image: "cookies",
    category: "Dessert",
    preparationTime: "3.5 hrs",
    numberOfServings: 16,
    description: "Tasty!",
    ingredients: "flour\nbutter\nchocolate\negg\nsugar",
    instructions: "1. Whip everything\n\n2. Enjoy!")
  #endif
}
7. DeepLinkHandler.swift
import Foundation

class DeepLinkHandler {
  // DeepLink enum with two cases:
  //  home - for navigating to the home view
  //  details - for navigating to a specific recipe detailed view
  //          - uses the parsed recipeID query from url for navigation
  enum DeepLink: Equatable {
    case home
    case details(recipeID: String)
  }

  // Parse url
  func parseComponents(from url: URL) -> DeepLink? {
    // 1
    guard url.scheme == "https" else {
      return nil
    }
    // 2
    guard url.pathComponents.contains("about") else {
      return .home
    }
    // 3
    guard let query = url.query else {
      return nil
    }
    // 4
    let components = query.split(separator: ",").flatMap {
      $0.split(separator: "=")
    }
    // 5
    guard let idIndex = components.firstIndex(of: Substring("recipeID")) else {
      return nil
    }
    // 6
    guard idIndex + 1 < components.count else {
      return nil
    }
    // 7
    return .details(recipeID: String(components[idIndex + 1]))
  }
}
8. DeepLinkKey.swift
import SwiftUI

struct DeepLinkKey: EnvironmentKey {
  static var defaultValue: DeepLinkHandler.DeepLink? {
    return nil
  }
}

// MARK: - Define a new environment value property
extension EnvironmentValues {
  var deepLink: DeepLinkHandler.DeepLink? {
    get {
      self[DeepLinkKey]
    }
    set {
      self[DeepLinkKey] = newValue
    }
  }
}
9. ActivityIndicatorView.swift
import SwiftUI

struct ActivityIndicatorView<Content>: View where Content: View {
  @Binding var isShowing: Bool
  var content: () -> Content

  var body: some View {
    ZStack(alignment: .center) {
      self.content()
        .disabled(self.isShowing)
        .blur(radius: self.isShowing ? 3 : 0)
      VStack {
        ProgressView("Sharing...")
          .progressViewStyle(CircularProgressViewStyle())
      }
      .frame(width: 150, height: 150)
      .background(Color.primary.colorInvert())
      .foregroundColor(.primary)
      .cornerRadius(20)
      .shadow(color: .secondary, radius: 10, x: 1, y: 0)
      .opacity(self.isShowing ? 1 : 0)
    }
  }
}
10. CodableBundleExtension.swift
import Foundation

// MARK: - JSON decode from bundle
extension Bundle {
  func decode(_ file: String) -> [Recipe] {
    guard let url = self.url(forResource: file, withExtension: nil) else {
      fatalError("Failed to locate \(file) in bundle.")
    }

    guard let data = try? Data(contentsOf: url) else {
      fatalError("Failed to load \(file) from bundle.")
    }

    let decoder = JSONDecoder()

    guard let loaded = try? decoder.decode([Recipe].self, from: data) else {
      fatalError("Failed to decode \(file) from bundle.")
    }

    return loaded
  }
}

后记

本篇主要讲述了Firebase Dynamic Links的简单使用,感兴趣的给个赞或者关注~~~

推荐阅读更多精彩内容