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

版本記錄

版本號(hào) 時(shí)間
V1.0 2019.12.28 星期六

前言

iOS中有關(guān)視圖控件用戶(hù)能看到的都在UIKit框架里面,用戶(hù)交互也是通過(guò)UIKit進(jìn)行的。感興趣的參考上面幾篇文章。
1. UIKit框架(一) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(一)
2. UIKit框架(二) —— UIKit動(dòng)力學(xué)和移動(dòng)效果(二)
3. UIKit框架(三) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(一)
4. UIKit框架(四) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(二)
5. UIKit框架(五) —— 自定義控件:可重復(fù)使用的滑塊(一)
6. UIKit框架(六) —— 自定義控件:可重復(fù)使用的滑塊(二)
7. UIKit框架(七) —— 動(dòng)態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(一)
8. UIKit框架(八) —— 動(dòng)態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(二)
9. UIKit框架(九) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(一)
10. UIKit框架(十) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(二)
11. UIKit框架(十一) —— UICollectionView的重用、選擇和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、選擇和重排序(二)
13. UIKit框架(十三) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(一)
14. UIKit框架(十四) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(二)
15. UIKit框架(十五) —— 基于自定義UICollectionViewLayout布局的簡(jiǎn)單示例(一)
16. UIKit框架(十六) —— 基于自定義UICollectionViewLayout布局的簡(jiǎn)單示例(二)
17. UIKit框架(十七) —— 基于自定義UICollectionViewLayout布局的簡(jiǎn)單示例(三)
18. UIKit框架(十八) —— 基于CALayer屬性的一種3D邊欄動(dòng)畫(huà)的實(shí)現(xiàn)(一)
19. UIKit框架(十九) —— 基于CALayer屬性的一種3D邊欄動(dòng)畫(huà)的實(shí)現(xiàn)(二)
20. UIKit框架(二十) —— 基于UILabel跑馬燈類(lèi)似效果的實(shí)現(xiàn)(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定義viewController的轉(zhuǎn)場(chǎng)和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定義viewController的轉(zhuǎn)場(chǎng)和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在兩個(gè)APP間的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在兩個(gè)APP間的使用示例 (二)
26. UIKit框架(二十六) —— UICollectionView的自定義布局 (一)
27. UIKit框架(二十七) —— UICollectionView的自定義布局 (二)
28. UIKit框架(二十八) —— 一個(gè)UISplitViewController的簡(jiǎn)單實(shí)用示例 (一)
29. UIKit框架(二十九) —— 一個(gè)UISplitViewController的簡(jiǎn)單實(shí)用示例 (二)
30. UIKit框架(三十) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的簡(jiǎn)單示例(一)
31. UIKit框架(三十一) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的簡(jiǎn)單示例(二)
32. UIKit框架(三十二) —— 替換Peek and Pop交互的基于iOS13的Context Menus(一)

源碼

1. Swift

首先看下工程組織結(jié)構(gòu)

接著,看下sb中的內(nèi)容

Main.storyboard
Map.storyboard
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,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容