UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二)

版本记录

版本号 时间
V1.0 2019.12.28 星期六

前言

iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)
18. UIKit框架(十八) —— 基于CALayer属性的一种3D边栏动画的实现(一)
19. UIKit框架(十九) —— 基于CALayer属性的一种3D边栏动画的实现(二)
20. UIKit框架(二十) —— 基于UILabel跑马灯类似效果的实现(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定义viewController的转场和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定义viewController的转场和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (二)
26. UIKit框架(二十六) —— UICollectionView的自定义布局 (一)
27. UIKit框架(二十七) —— UICollectionView的自定义布局 (二)
28. UIKit框架(二十八) —— 一个UISplitViewController的简单实用示例 (一)
29. UIKit框架(二十九) —— 一个UISplitViewController的简单实用示例 (二)
30. UIKit框架(三十) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(一)
31. UIKit框架(三十一) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(二)
32. UIKit框架(三十二) —— 替换Peek and Pop交互的基于iOS13的Context Menus(一)

源码

1. Swift

首先看下工程组织结构

UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二)_第1张图片

接着,看下sb中的内容

UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二)_第2张图片
Main.storyboard
UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二)_第3张图片
Map.storyboard
UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二)_第4张图片
Rating.storyboard

下面就是代码了

1. SpotsViewController.swift
import UIKit
import MapKit

class SpotsViewController: UITableViewController {
  var vacationSpots: [VacationSpot] = []
  
  // MARK: - Lifecycle
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    vacationSpots = VacationSpot.defaultSpots
  }
  
  // MARK: - Navigation
  
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard
      let selectedCell = sender as? UITableViewCell,
      let selectedRowIndex = tableView.indexPath(for: selectedCell)?.row,
      segue.identifier == "showSpotInfoViewController"
      else {
        fatalError("sender is not a UITableViewCell or was not found in the tableView, or segue.identifier is incorrect")
    }
    
    let vacationSpot = vacationSpots[selectedRowIndex]
    let detailViewController = segue.destination as! SpotInfoViewController
    detailViewController.vacationSpot = vacationSpot
  }
  
  func showMap(vacationSpot: VacationSpot) {
    let storyboard = UIStoryboard(name: "Map", bundle: nil)
    
    let initial = storyboard.instantiateInitialViewController()
    guard
      let navigationController =  initial as? UINavigationController,
      let mapViewController = navigationController.topViewController
        as? MapViewController
      else {
        fatalError("Unexpected view hierarchy")
    }
    
    mapViewController.locationToShow = vacationSpot.coordinate
    mapViewController.title = vacationSpot.name
    
    present(navigationController, animated: true)
  }
  
  // MARK: - UITableViewDataSource
  
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return vacationSpots.count
  }
  
  override func tableView(_ tableView: UITableView,
                          cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "VacationSpotCell",
                                             for: indexPath) as! VacationSpotCell
    let vacationSpot = vacationSpots[indexPath.row]
    cell.nameLabel.text = vacationSpot.name
    cell.locationNameLabel.text = vacationSpot.locationName
    cell.thumbnailImageView.image = UIImage(named: vacationSpot.thumbnailName)
    
    return cell
  }
  
  // MARK: - UITableViewDelegate
  
  override func tableView(
    _ tableView: UITableView,
    contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint)
    -> UIContextMenuConfiguration? {
      // 1
      let index = indexPath.row
      let vacationSpot = vacationSpots[index]
      
      // 2
      let identifier = "\(index)" as NSString
      
      return UIContextMenuConfiguration(
      identifier: identifier, previewProvider: nil) { _ in
        // 3
        let mapAction = UIAction(title: "View map",
                                 image: UIImage(systemName: "map")) { _ in
                                  self.showMap(vacationSpot: vacationSpot)
        }
        
        // 4
        let shareAction = UIAction(
          title: "Share",
          image: UIImage(systemName: "square.and.arrow.up")) { _ in
            VacationSharer.share(vacationSpot: vacationSpot, in: self)
        }
        
        // 5
        return UIMenu(title: "", image: nil,
                      children: [mapAction, shareAction])
      }
  }
  
  override func tableView(_ tableView: UITableView,
                          previewForHighlightingContextMenuWithConfiguration
    configuration: UIContextMenuConfiguration)
    -> UITargetedPreview? {
      guard
        // 1
        let identifier = configuration.identifier as? String,
        let index = Int(identifier),
        // 2
        let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0))
          as? VacationSpotCell
        else {
          return nil
      }
      
      // 3
      return UITargetedPreview(view: cell.thumbnailImageView)
  }
  
  override func tableView(
    _ tableView: UITableView, willPerformPreviewActionForMenuWith
    configuration: UIContextMenuConfiguration,
    animator: UIContextMenuInteractionCommitAnimating) {
    // 1
    guard
      let identifier = configuration.identifier as? String,
      let index = Int(identifier)
      else {
        return
    }
    
    // 2
    let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0))
    
    // 3
    animator.addCompletion {
      self.performSegue(withIdentifier: "showSpotInfoViewController",
                        sender: cell)
    }
  }
}
2. SpotInfoViewController.swift
import UIKit
import SafariServices

class SpotInfoViewController: UIViewController {
  var vacationSpot: VacationSpot!
  
  @IBOutlet var backgroundColoredViews: [UIView]!
  @IBOutlet var headingLabels: [UILabel]!
  
  @IBOutlet var ownRatingStackView: UIStackView!
  @IBOutlet var whyVisitLabel: UILabel!
  @IBOutlet var whatToSeeLabel: UILabel!
  @IBOutlet var weatherInfoLabel: UILabel!
  @IBOutlet var averageRatingLabel: UILabel!
  @IBOutlet var ownRatingLabel: UILabel!
  @IBOutlet var weatherHideOrShowButton: UIButton!
  @IBOutlet var submitRatingButton: UIButton!
  
  var shouldHideWeatherInfoSetting: Bool {
    get {
      return UserDefaults.standard.bool(forKey: "shouldHideWeatherInfo")
    }
    set {
      UserDefaults.standard.set(newValue, forKey: "shouldHideWeatherInfo")
    }
  }
  
  var currentUserRating: Int {
    get {
      return UserDefaults.standard.integer(
        forKey: "currentUserRating-\(vacationSpot.identifier)")
    }
    set {
      UserDefaults.standard.set(
        newValue, forKey: "currentUserRating-\(vacationSpot.identifier)")
      updateCurrentRating()
    }
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // Clear background colors from labels and buttons
    for view in backgroundColoredViews {
      view.backgroundColor = .clear
    }
    
    // Set the kerning to 1 to increase spacing between letters
    headingLabels.forEach { $0.attributedText = NSAttributedString(
      string: $0.text!, attributes: [NSAttributedString.Key.kern: 1]) }
    
    title = vacationSpot.name
    
    whyVisitLabel.text = vacationSpot.whyVisit
    whatToSeeLabel.text = vacationSpot.whatToSee
    weatherInfoLabel.text = vacationSpot.weatherInfo
    averageRatingLabel.text = String(repeating: "★", count: vacationSpot.userRating)
    
    updateWeatherInfoViews(hideWeatherInfo: shouldHideWeatherInfoSetting,
                           animated: false)
    
    let interaction = UIContextMenuInteraction(delegate: self)
    submitRatingButton.addInteraction(interaction)
  }
  
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    updateCurrentRating()
  }
  
  private func updateCurrentRating() {
    UIView.animate(withDuration: 0.3) {
      let rating = self.currentUserRating
      if rating > 0 {
        self.submitRatingButton.setTitle("Update Rating (\(rating))",
          for: .normal)
        self.ownRatingStackView.isHidden = false
        self.ownRatingLabel.text = String(repeating: "★",
                                          count: rating)
      } else {
        self.submitRatingButton.setTitle("Submit Rating", for: .normal)
        self.ownRatingStackView.isHidden = true
      }
    }
  }
  
  @IBAction func weatherHideOrShowButtonTapped(_ sender: UIButton) {
    let shouldHideWeatherInfo = sender.titleLabel!.text! == "Hide"
    updateWeatherInfoViews(hideWeatherInfo: shouldHideWeatherInfo,
                           animated: true)
    shouldHideWeatherInfoSetting = shouldHideWeatherInfo
  }
  
  func updateWeatherInfoViews(hideWeatherInfo shouldHideWeatherInfo: Bool,
                              animated: Bool) {
    let newButtonTitle = shouldHideWeatherInfo ? "Show" : "Hide"
    
    if animated {
      UIView.animate(withDuration: 0.3) {
        self.weatherHideOrShowButton.setTitle(newButtonTitle, for: .normal)
        self.weatherInfoLabel.isHidden = shouldHideWeatherInfo
      }
    } else {
      weatherHideOrShowButton.setTitle(newButtonTitle, for: .normal)
      weatherInfoLabel.isHidden = shouldHideWeatherInfo
    }
  }
  
  @IBAction func wikipediaButtonTapped(_ sender: UIButton) {
    let safariVC = SFSafariViewController(url: vacationSpot.wikipediaURL)
    safariVC.delegate = self
    present(safariVC, animated: true, completion: nil)
  }
  
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    switch segue.identifier! {
    case "presentMapViewController":
      guard
        let navigationController = segue.destination as? UINavigationController,
        let mapViewController = navigationController.topViewController
          as? MapViewController
        else {
          fatalError("Unexpected view hierarchy")
      }
      mapViewController.locationToShow = vacationSpot.coordinate
      mapViewController.title = vacationSpot.name
    case "presentRatingViewController":
      guard
        let navigationController = segue.destination as? UINavigationController,
        let ratingViewController = navigationController.topViewController
          as? RatingViewController
        else {
          fatalError("Unexpected view hierarchy")
      }
      ratingViewController.vacationSpot = vacationSpot
      ratingViewController.onComplete = updateCurrentRating
    default:
      fatalError("Unhandled Segue: \(segue.identifier!)")
    }
  }
}

// MARK: - SFSafariViewControllerDelegate
extension SpotInfoViewController: SFSafariViewControllerDelegate {
  func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
    controller.dismiss(animated: true, completion: nil)
  }
}

// MARK: - UIContextMenuInteractionDelegate
extension SpotInfoViewController: UIContextMenuInteractionDelegate {
  func contextMenuInteraction(_ interaction: UIContextMenuInteraction,
                              configurationForMenuAtLocation location: CGPoint)
    -> UIContextMenuConfiguration? {
      return UIContextMenuConfiguration(
        identifier: nil,
        previewProvider: makeRatePreview) { _ in
          let removeRating = self.makeRemoveRatingAction()
          let rateMenu = self.makeRateMenu()
          let children = [rateMenu, removeRating]
          return UIMenu(title: "", children: children)
      }
  }
  
  func makeRemoveRatingAction() -> UIAction {
    // 1
    var removeRatingAttributes = UIMenuElement.Attributes.destructive
    
    // 2
    if currentUserRating == 0 {
      removeRatingAttributes.insert(.disabled)
    }
    
    // 3
    let deleteImage = UIImage(systemName: "delete.left")
    
    // 4
    return UIAction(title: "Remove rating",
                    image: deleteImage,
                    identifier: nil,
                    attributes: removeRatingAttributes) { _ in
                      self.currentUserRating = 0
    }
  }
  
  func updateRating(from action: UIAction) {
    guard let number = Int(action.identifier.rawValue) else {
      return
    }
    currentUserRating = number
  }
  
  func makeRateMenu() -> UIMenu {
    let ratingButtonTitles = ["Boring", "Meh", "It's OK", "Like It", "Fantastic!"]
    
    let rateActions = ratingButtonTitles
      .enumerated()
      .map { index, title in
        return UIAction(title: title,
                        identifier: UIAction.Identifier("\(index + 1)"),
                        handler: updateRating)
      }
    
    return UIMenu(title: "Rate...",
                  image: UIImage(systemName: "star.circle"),
                  options: .displayInline,
                  children: rateActions)
  }
  
  func makeRatePreview() -> UIViewController {
    let viewController = UIViewController()
    
    // 1
    let imageView = UIImageView(image: UIImage(named: "rating_star"))
    viewController.view = imageView
    
    // 2
    imageView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
    imageView.translatesAutoresizingMaskIntoConstraints = false
    
    // 3
    viewController.preferredContentSize = imageView.frame.size
    
    return viewController
  }
}
3. RatingViewController.swift
import UIKit

class RatingViewController: UIViewController {
  var vacationSpot: VacationSpot!
  var onComplete: () -> Void = { }
  
  @IBOutlet var questionLabel: UILabel!
  @IBOutlet var ratingButtons: [UIButton]!
  @IBOutlet var starsStackView: UIStackView!
  @IBOutlet var submitRatingButton: UIButton!
  @IBOutlet var deleteRatingButton: UIButton!
  
  var currentUserRating: Int {
    get {
      return UserDefaults.standard.integer(forKey: "currentUserRating-\(vacationSpot.identifier)")
    }
    set {
      UserDefaults.standard.set(newValue, forKey: "currentUserRating-\(vacationSpot.identifier)")
    }
  }
  
  let ratingButtonTitles = ["Boring", "Meh", "It's OK", "Like It", "Fantastic!"]
  
  // MARK: - Lifecycle
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // Clear storyboard background colors
    for button in ratingButtons {
      button.backgroundColor = .clear
    }
    
    questionLabel.text = "How would you rate \(vacationSpot.name)?"
    
    showStarCount(currentUserRating, animated: false)
    
    deleteRatingButton.isHidden = currentUserRating == 0
    
    if currentUserRating > 0 {
      submitRatingButton.setTitle("Update Your Rating", for: .normal)
      let index = currentUserRating - 1
      let titleOfButtonToSelect = ratingButtonTitles[index]
      for ratingButton in ratingButtons {
        ratingButton.isSelected = ratingButton.titleLabel!.text! == titleOfButtonToSelect
      }
    }
  }
  
  override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
    super.dismiss(animated: flag, completion: completion)
    onComplete()
  }
  
  // MARK: - Actions
  
  @IBAction func cancelButtonTapped(_ sender: UIBarButtonItem) {
    dismiss(animated: true, completion: nil)
  }
  
  @IBAction func submitRatingTapped() {
    currentUserRating = starsStackView.arrangedSubviews.count
    dismiss(animated: true, completion: nil)
  }
  
  @IBAction func deleteRatingTapped() {
    currentUserRating = 0
    dismiss(animated: true, completion: nil)
  }
  
  @IBAction func ratingButtonTapped(_ sender: UIButton) {
    let buttonTitle = sender.titleLabel!.text!
    
    // Select the tapped button and unselect others
    for ratingButton in ratingButtons {
      ratingButton.isSelected = ratingButton == sender
    }
    
    let rating = ratingForButtonTitle(buttonTitle)
    showStarCount(rating)
  }
  
  // MARK: - Helper Methods
  
  func ratingForButtonTitle(_ buttonTitle: String) -> Int {
    guard let index = ratingButtonTitles.firstIndex(of: buttonTitle) else {
      fatalError("Rating not found for buttonTitle: \(buttonTitle)")
    }
    return index + 1
  }
  
  func showStarCount(_ totalStarCount: Int, animated: Bool = true) {
    let starsToAdd = totalStarCount - starsStackView.arrangedSubviews.count
    
    if starsToAdd > 0 {
      for _ in 1...starsToAdd {
        let starImageView = UIImageView(image: UIImage(named: "rating_star"))
        starImageView.contentMode = .scaleAspectFit
        starImageView.frame.origin = CGPoint(x: starsStackView.frame.width, y: 0) // animate in from the right
        starsStackView.addArrangedSubview(starImageView)
      }
    } else if starsToAdd < 0 {
      let starsToRemove = abs(starsToAdd)
      
      for _ in 1...starsToRemove {
        guard let star = starsStackView.arrangedSubviews.last else {
          fatalError("Unexpected Logic Error")
        }
        star.removeFromSuperview() // No need to call removeArrangedSubview separately
      }
    }
    
    if animated {
      UIView.animate(withDuration: 0.25) {
        self.starsStackView.layoutIfNeeded()
      }
    }
  }
}
4. MapViewController.swift
import UIKit
import MapKit

class MapViewController: UIViewController {
  var locationToShow: CLLocationCoordinate2D!

  @IBOutlet var mapView: MKMapView!

  override func viewDidLoad() {
    super.viewDidLoad()

    mapView.setCenter(locationToShow, animated: true)

    let zoomRegion = MKCoordinateRegion.init(center: locationToShow, latitudinalMeters: 15000, longitudinalMeters: 15000)
    mapView.setRegion(zoomRegion, animated: true)

    let annotation = MKPointAnnotation()
    annotation.coordinate = locationToShow
    mapView.addAnnotation(annotation)
  }

  @IBAction func doneButtonTapped(_ sender: UIBarButtonItem) {
    dismiss(animated: true, completion: nil)
  }
}
5. VacationSpot.swift
import Foundation
import MapKit

struct VacationSpot {
  let identifier: Int
  let name: String
  let locationName: String
  let thumbnailName: String
  let whyVisit: String
  let whatToSee: String
  let weatherInfo: String
  let userRating: Int
  let wikipediaURL: URL
  let coordinate: CLLocationCoordinate2D
}

extension VacationSpot: Codable {
  enum CodingKeys: String, CodingKey {
    case identifier
    case name
    case locationName
    case thumbnailName
    case whyVisit
    case whatToSee
    case weatherInfo
    case userRating
    case wikipediaLink
    case latitude
    case longitude
  }
  
  public func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(identifier, forKey: .identifier)
    try container.encode(name, forKey: .name)
    try container.encode(locationName, forKey: .locationName)
    try container.encode(thumbnailName, forKey: .thumbnailName)
    try container.encode(whyVisit, forKey: .whyVisit)
    try container.encode(whatToSee, forKey: .whatToSee)
    try container.encode(weatherInfo, forKey: .weatherInfo)
    try container.encode(userRating, forKey: .userRating)
    try container.encode(wikipediaURL, forKey: .wikipediaLink)
    try container.encode(coordinate.latitude, forKey: .latitude)
    try container.encode(coordinate.longitude, forKey: .longitude)
  }
  
  public init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    
    identifier = try values.decode(Int.self, forKey: .identifier)
    name = try values.decode(String.self, forKey: .name)
    locationName = try values.decode(String.self, forKey: .locationName)
    thumbnailName = try values.decode(String.self, forKey: .thumbnailName)
    whyVisit = try values.decode(String.self, forKey: .whyVisit)
    whatToSee = try values.decode(String.self, forKey: .whatToSee)
    weatherInfo = try values.decode(String.self, forKey: .weatherInfo)
    userRating = try values.decode(Int.self, forKey: .userRating)
    
    let wikipediaLink = try values.decode(String.self, forKey: .wikipediaLink)
    guard let wikiURL = URL(string: wikipediaLink) else {
      fatalError("Invalid Wikipedia URL.")
    }
    wikipediaURL = wikiURL
      
    let latitude = try values.decode(Double.self, forKey: .latitude)
    let longitude = try values.decode(Double.self, forKey: .longitude)
    coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
  }
}

// MARK: - Support for loading data from plist

extension VacationSpot {
  static let defaultSpots = loadVacationSpotsFromPlistNamed("vacation_spots")
  
  static func loadVacationSpotsFromPlistNamed(_ plistName: String) -> [VacationSpot] {
    guard
      let plistURL = Bundle.main.url(forResource: plistName, withExtension: "plist"),
      let data = try? Data(contentsOf: plistURL)
      else {
        fatalError("An error occurred while reading \(plistName).plist")
    }
    
    let decoder = PropertyListDecoder()
    
    do {
      let vacationSpots = try decoder.decode([VacationSpot].self, from: data)
      return vacationSpots
    } catch {
      print("Couldn't load vacation spots: \(error.localizedDescription)")
      return []
    }
  }
}
6. VacationSpotCell.swift
import UIKit

class VacationSpotCell: UITableViewCell {
  @IBOutlet var nameLabel: UILabel!
  @IBOutlet var locationNameLabel: UILabel!
  @IBOutlet var thumbnailImageView: UIImageView!

  override func awakeFromNib() {
    super.awakeFromNib()

    // 1
    let layoutGuide = UILayoutGuide()
    contentView.addLayoutGuide(layoutGuide)

    // 2
    let topConstraint = layoutGuide.topAnchor
      .constraint(equalTo: nameLabel.topAnchor)

    // 3
    let bottomConstraint = layoutGuide.bottomAnchor
      .constraint(equalTo: locationNameLabel.bottomAnchor)

    // 4
    let centeringConstraint = layoutGuide.centerYAnchor
      .constraint(equalTo: contentView.centerYAnchor)

    // 5
    NSLayoutConstraint.activate(
      [topConstraint, bottomConstraint, centeringConstraint])
  }
}
7. VacationSharer.swift
import UIKit

struct VacationSharer {
  static func share(vacationSpot: VacationSpot,
                    in viewController: UIViewController) {
    let text = """
    You should really visit \(vacationSpot.name)!
    
    \(vacationSpot.whyVisit)
    
    \(vacationSpot.whatToSee)
    """
    
    guard let image = UIImage(named: vacationSpot.thumbnailName) else {
      return
    }
    
    let activityViewController = UIActivityViewController(
      activityItems: [text, image], applicationActivities: nil)
    
    viewController.present(activityViewController, animated: true, completion: nil)
  }
  
}

后记

本篇主要讲述了替换旧的Peek and Pop交互的基于iOS13的Context Menus,感兴趣的给个赞或者关注~~~

UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二)_第5张图片

你可能感兴趣的:(UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二))