版本记录
版本号 | 时间 |
---|---|
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
首先看下工程组织结构
接着,看下sb中的内容
下面就是代码了
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
,感兴趣的给个赞或者关注~~~