版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2019.12.28 星期六 |
前言
iOS中有關視圖控件用戶能看到的都在UIKit框架里面抛人,用戶交互也是通過UIKit進行的棍丐。感興趣的參考上面幾篇文章掘譬。
1. UIKit框架(一) —— UIKit動力學和移動效果(一)
2. UIKit框架(二) —— UIKit動力學和移動效果(二)
3. UIKit框架(三) —— UICollectionViewCell的擴張效果的實現(xiàn)(一)
4. UIKit框架(四) —— UICollectionViewCell的擴張效果的實現(xiàn)(二)
5. UIKit框架(五) —— 自定義控件:可重復使用的滑塊(一)
6. UIKit框架(六) —— 自定義控件:可重復使用的滑塊(二)
7. UIKit框架(七) —— 動態(tài)尺寸UITableViewCell的實現(xiàn)(一)
8. UIKit框架(八) —— 動態(tài)尺寸UITableViewCell的實現(xiàn)(二)
9. UIKit框架(九) —— UICollectionView的數(shù)據(jù)異步預加載(一)
10. UIKit框架(十) —— UICollectionView的數(shù)據(jù)異步預加載(二)
11. UIKit框架(十一) —— UICollectionView的重用寥掐、選擇和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用胀屿、選擇和重排序(二)
13. UIKit框架(十三) —— 如何創(chuàng)建自己的側滑式面板導航(一)
14. UIKit框架(十四) —— 如何創(chuàng)建自己的側滑式面板導航(二)
15. UIKit框架(十五) —— 基于自定義UICollectionViewLayout布局的簡單示例(一)
16. UIKit框架(十六) —— 基于自定義UICollectionViewLayout布局的簡單示例(二)
17. UIKit框架(十七) —— 基于自定義UICollectionViewLayout布局的簡單示例(三)
18. UIKit框架(十八) —— 基于CALayer屬性的一種3D邊欄動畫的實現(xiàn)(一)
19. UIKit框架(十九) —— 基于CALayer屬性的一種3D邊欄動畫的實現(xiàn)(二)
20. UIKit框架(二十) —— 基于UILabel跑馬燈類似效果的實現(xiàn)(一)
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中的內(nèi)容
下面就是代碼了
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
费奸,感興趣的給個贊或者關注~~~