UIKit框架(五十) —— UIVisualEffectView原理和简单使用(二)

版本记录

版本号 时间
V1.0 2020.11.29 星期日

前言

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(一)
33. UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二)
34. UIKit框架(三十四) —— Accessibility的使用(一)
35. UIKit框架(三十五) —— Accessibility的使用(二)
36. UIKit框架(三十六) —— UICollectionView UICollectionViewDiffableDataSource的使用(一)
37. UIKit框架(三十七) —— UICollectionView UICollectionViewDiffableDataSource的使用(二)
38. UIKit框架(三十八) —— 基于CollectionView转盘效果的实现(一)
39. UIKit框架(三十九) —— iOS 13中UISearchController 和 UISearchBar的新更改(一)
40. UIKit框架(四十) —— iOS 13中UISearchController 和 UISearchBar的新更改(二)
41. UIKit框架(四十一) —— 使用协议构建自定义Collection(一)
42. UIKit框架(四十二) —— 使用协议构建自定义Collection(二)
43. UIKit框架(四十三) —— CALayer的简单实用示例(一)
44. UIKit框架(四十四) —— CALayer的简单实用示例(二)
45. UIKit框架(四十五) —— 支持DarkMode的简单示例(一)
46. UIKit框架(四十六) —— 支持DarkMode的简单示例(二)
47. UIKit框架(四十七) —— 自定义Calendar Control的简单示例(一)
48. UIKit框架(四十八) —— 自定义Calendar Control的简单示例(二)
49. UIKit框架(四十九) —— UIVisualEffectView原理和简单使用(一)

源码

1. Swift

首先看下工程组织结构:

下面就是源码啦

1. ThemedNavigationController.swift
import UIKit

class ThemedNavigationController: UINavigationController {
  override func viewDidLoad() {
    super.viewDidLoad()

    NotificationCenter
      .default
      .addObserver(
        self,
        selector: #selector(applyTheme),
        name: themeDidChangeNotification,
        object: nil)
  }

  @objc func applyTheme() {
    let theme = Theme.shared
    navigationBar.barTintColor = theme.barTintColor
    view.tintColor = theme.tintColor
  }
}
2. StoryListViewController.swift
import UIKit

class StoryListViewController: UITableViewController {
  private var stories: [Story] = [] {
    didSet {
      tableView.reloadData()
    }
  }

  @IBSegueAction func makeStoryViewController(_ coder: NSCoder) -> StoryViewController? {
    guard let row = tableView.indexPathForSelectedRow?.row else {
      return nil
    }
    let story = stories[row]
    return StoryViewController(story: story, coder: coder)
  }
}

// MARK: - Lifecycle
extension StoryListViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    registerForNotifications()

    let image = UIImage(systemName: "book.circle.fill")
    let imageView = UIImageView(image: image)
    imageView.frame.size = CGSize(width: 32, height: 32)
    navigationItem.titleView = imageView
    applyTheme()

    tableView.rowHeight = UITableView.automaticDimension
    tableView.estimatedRowHeight = 79

    Story.loadStories { [unowned self] loadedStories in
      self.stories = loadedStories
    }
  }
}

// MARK: - Privates
private extension StoryListViewController {
  func registerForNotifications() {
    let notificationCenter = NotificationCenter.default
    notificationCenter
      .addObserver(
        self,
        selector: #selector(StoryViewController.preferredContentSizeDidChange(forChildContentContainer:)),
        name: UIContentSizeCategory.didChangeNotification,
        object: nil)

    notificationCenter
      .addObserver(
        self,
        selector: #selector(applyTheme),
        name: themeDidChangeNotification,
        object: nil)
  }

  @objc func applyTheme() {
    tableView.separatorColor = Theme.shared.separatorColor
    tableView.reloadData()
  }
}

// MARK: - UITableViewDataSource
extension StoryListViewController {
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return stories.count
  }

  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let storyCell = tableView.dequeueReusableCell(withIdentifier: "StoryCell", for: indexPath) as? StoryCell
    storyCell?.story = stories[indexPath.row]
    return storyCell ?? UITableViewCell()
  }
}
3. StoryViewController.swift
import UIKit

class StoryViewController: UIViewController {
  @IBOutlet weak var storyView: StoryView!
  @IBOutlet weak var optionsContainerView: UIView!
  @IBOutlet weak var optionsContainerViewBottomConstraint: NSLayoutConstraint!

  private var showingOptions = false
  private let story: Story

  required init?(coder: NSCoder) {
    fatalError("init(coder:) is not implemented")
  }

  init?(story: Story, coder: NSCoder) {
    self.story = story
    super.init(coder: coder)
  }
}

// MARK: - Lifecycle
extension StoryViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    let image = UIImage(systemName: "book.circle.fill")
    let imageView = UIImageView(image: image)
    imageView.frame.size = CGSize(width: 32, height: 32)
    navigationItem.titleView = imageView

    storyView.story = story

    applyTheme()

    NotificationCenter
      .default
      .addObserver(
        self,
        selector: #selector(applyTheme),
        name: themeDidChangeNotification,
        object: nil)
  }

  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    setOptionsHidden(true, animated: false)
  }
}

// MARK: - Privates
private extension StoryViewController {
  @IBAction func optionsButtonTapped() {
    setOptionsHidden(showingOptions, animated: true)
  }

  func setOptionsHidden(_ hidden: Bool, animated: Bool) {
    showingOptions = !hidden
    let height = optionsContainerView.bounds.height
    let constant = hidden ? -height : view.safeAreaInsets.bottom
    view.layoutIfNeeded()

    optionsContainerViewBottomConstraint.constant = constant
    if animated {
      UIView.animate(withDuration: 0.2) {
        self.view.layoutIfNeeded()
      }
    }
  }

  @objc func applyTheme() {
    let theme = Theme.shared
    view.backgroundColor = theme.textBackgroundColor
    children.forEach { viewController in
      viewController.view.tintColor = theme.tintColor
    }
    storyView.applyTheme()
  }
}
4. OptionsViewController.swift
import UIKit

class OptionsViewController: UIViewController {
  private var currentPage = 0
  @IBOutlet weak var readingModeSegmentedControl: UISegmentedControl!
  @IBOutlet weak var scrollView: UIScrollView!
  @IBOutlet weak var titleAlignmentSegmentedControl: UISegmentedControl!
  @IBOutlet weak var pageControl: UIPageControl!
  @IBOutlet weak var optionsView: UIView!
}

// MARK: - Lifecycle
extension OptionsViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    scrollView.scrollsToTop = false

    guard !UIAccessibility.isReduceTransparencyEnabled else {
      return
    }
    view.backgroundColor = .clear
    let blurEffect = UIBlurEffect(style: .dark)
    let blurView = UIVisualEffectView(effect: blurEffect)
    blurView.translatesAutoresizingMaskIntoConstraints = false
    view.insertSubview(blurView, at: 0)
    NSLayoutConstraint.activate([
      blurView.topAnchor.constraint(equalTo: view.topAnchor),
      blurView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
      blurView.heightAnchor.constraint(equalTo: view.heightAnchor),
      blurView.widthAnchor.constraint(equalTo: view.widthAnchor)
    ])

    let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
    let vibrancyView = UIVisualEffectView(effect: vibrancyEffect)
    vibrancyView.translatesAutoresizingMaskIntoConstraints = false
    vibrancyView.contentView.addSubview(optionsView)
    blurView.contentView.addSubview(vibrancyView)

    NSLayoutConstraint.activate([
      vibrancyView.heightAnchor.constraint(equalTo: blurView.contentView.heightAnchor),
      vibrancyView.widthAnchor.constraint(equalTo: blurView.contentView.widthAnchor),
      vibrancyView.centerXAnchor.constraint(equalTo: blurView.contentView.centerXAnchor),
      vibrancyView.centerYAnchor.constraint(equalTo: blurView.contentView.centerYAnchor)
    ])

    NSLayoutConstraint.activate([
      optionsView.centerXAnchor.constraint(equalTo: vibrancyView.contentView.centerXAnchor),
      optionsView.centerYAnchor.constraint(equalTo: vibrancyView.contentView.centerYAnchor)
    ])
  }

  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    let theme = Theme.shared
    readingModeSegmentedControl.selectedSegmentIndex = theme.readingMode.rawValue
    titleAlignmentSegmentedControl.selectedSegmentIndex = theme.titleAlignment.rawValue
    currentPage = theme.font.rawValue
    pageControl.currentPage = currentPage
    synchronizeViews(scrolled: false)
  }
}

// MARK: - Actions
extension OptionsViewController {
  @IBAction func pageControlPageDidChange() {
    synchronizeViews(scrolled: false)
  }

  @IBAction func readingModeDidChange(_ segmentedControl: UISegmentedControl) {
    Theme.shared.readingMode = ReadingMode(rawValue: segmentedControl.selectedSegmentIndex) ?? .dayTime
  }

  @IBAction func titleAlignmentDidChange(_ segmentedControl: UISegmentedControl) {
    Theme.shared.titleAlignment = TitleAlignment(rawValue: segmentedControl.selectedSegmentIndex) ?? .left
  }

  private func synchronizeViews(scrolled: Bool) {
    let pageWidth = scrollView.bounds.width
    var page: Int = 0
    var offset: CGFloat = 0

    if scrolled {
      offset = scrollView.contentOffset.x
      page = Int(offset / pageWidth)
      pageControl.currentPage = page
    } else {
      page = pageControl.currentPage
      offset = CGFloat(page) * pageWidth
      scrollView.setContentOffset(CGPoint(x: offset, y: 0), animated: true)
    }

    if page != currentPage {
      currentPage = page
      Theme.shared.font = Font(rawValue: currentPage) ?? .firaSans
    }
  }
}

// MARK: - UIScrollViewDelegate
extension OptionsViewController: UIScrollViewDelegate {
  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if scrollView.isDragging || scrollView.isDecelerating {
      synchronizeViews(scrolled: true)
    }
  }
}
5. StoryCell.swift
import UIKit

class StoryCell: UITableViewCell {
  @IBOutlet weak var titleLabel: UILabel!
  @IBOutlet weak var previewLabel: UILabel!

  var story: Story? {
    didSet {
      guard let story = story else { return }
      titleLabel.text = story.title
      previewLabel.text = story.content
      applyTheme()
    }
  }

  func applyTheme() {
    let theme = Theme.shared

    backgroundColor = theme.textBackgroundColor
    contentView.backgroundColor = theme.textBackgroundColor

    titleLabel.backgroundColor = theme.textBackgroundColor
    titleLabel.font = theme.preferredFont(forTextStyle: .headline)
    titleLabel.textColor = theme.textColor

    previewLabel.backgroundColor = theme.textBackgroundColor
    previewLabel.font = theme.preferredFont(forTextStyle: .body)
    previewLabel.textColor = theme.textColor
  }
}
6. StoryView.swift
import UIKit

class StoryView: UIView {
  @IBOutlet var titleLabel: UILabel!
  @IBOutlet var contentLabel: UILabel!
  @IBOutlet var separatorView: UIView!

  var story: Story? {
    didSet {
      titleLabel.text = story?.title
      contentLabel.text = story?.content
    }
  }

  override func awakeFromNib() {
    super.awakeFromNib()
    applyTheme()
  }

  func applyTheme() {
    let theme = Theme.shared
    backgroundColor = theme.textBackgroundColor

    titleLabel.backgroundColor = theme.textBackgroundColor
    titleLabel.font = theme.preferredFont(forTextStyle: .headline)
    titleLabel.textColor = theme.textColor
    titleLabel.textAlignment = theme.titleAlignment == TitleAlignment.center
      ? NSTextAlignment.center : NSTextAlignment.left

    contentLabel.backgroundColor = theme.textBackgroundColor
    contentLabel.font = theme.preferredFont(forTextStyle: .body)
    contentLabel.textColor = theme.textColor

    separatorView.backgroundColor = theme.separatorColor
  }
}
7. Theme.swift
import UIKit

let themeDidChangeNotification = Notification.Name("ThemeDidChangeNotification")

class Theme {
  static var shared = Theme()

  private let bodyFonts = [
    "FiraSans-Regular",
    "NotoSans",
    "OpenSans",
    "PTSans-Regular",
    "SourceSansPro-Regular"
  ]

  private let headlineFonts = [
    "FiraSans-SemiBold",
    "NotoSans-Bold",
    "OpenSans-Semibold",
    "PTSans-Bold",
    "SourceSansPro-Semibold"
  ]

  private let textBackgroundColors = [UIColor.white, UIColor.nightTimeTextBackground]
  private let textColors = [UIColor.darkText, UIColor.nightTimeText]

  var font: Font = .firaSans {
    didSet {
      notifyObservers()
    }
  }

  var readingMode: ReadingMode = .dayTime {
    didSet {
      notifyObservers()
    }
  }
  var titleAlignment: TitleAlignment = .center {
    didSet {
      notifyObservers()
    }
  }

  var barTintColor: UIColor {
    let color = textBackgroundColors[readingMode.rawValue]
    return color.colorForTranslucency()
  }

  var tintColor: UIColor? {
    return readingMode == .dayTime ? nil : UIColor.nightTimeTint
  }

  var separatorColor: UIColor {
    return readingMode == .dayTime ? UIColor.defaultSeparator : UIColor.nightTimeTint
  }

  var textBackgroundColor: UIColor {
    return textBackgroundColors[readingMode.rawValue]
  }

  var textColor: UIColor {
    return textColors[readingMode.rawValue]
  }

  func preferredFont(forTextStyle style: UIFont.TextStyle) -> UIFont {
    let systemFont = UIFont.preferredFont(forTextStyle: style)

    if font == .system {
      return systemFont
    }

    let size = systemFont.pointSize
    var preferredFont: UIFont?

    switch style {
    case .headline:
      preferredFont = UIFont(name: headlineFonts[font.rawValue], size: size)
    default:
      preferredFont = UIFont(name: bodyFonts[font.rawValue], size: size)
    }

    return preferredFont ?? systemFont
  }

  private func notifyObservers() {
    NotificationCenter.default.post(name: themeDidChangeNotification, object: nil)
  }
}
8. Font.swift
import Foundation

enum Font: Int {
  case firaSans, notoSans, openSans, pztSans, sourceSansPro, system
}
9. ReadingMode.swift
import Foundation

enum ReadingMode: Int {
  case dayTime, nightTime
}
10. TitleAlignment.swift
import Foundation

enum TitleAlignment: Int {
  case center, left
}
11. Story.swift
import Foundation

struct Story {
  let title: String
  let content: String

  static func loadStories(_ completion: (@escaping ([Story]) -> Void)) {
    let path = Bundle.main.bundlePath
    let manager = FileManager.default

    var stories: [Story] = []

    if var contents = try? manager.contentsOfDirectory(atPath: path) {
      contents = contents.sorted(by: <)

      for file in contents {
        if file.hasSuffix(".grm") {
          guard let filePath = URL(string: "file://" + path)?.appendingPathComponent(file) else { continue }
          let title = String(file.split(separator: ".", maxSplits: 1, omittingEmptySubsequences: true)[0])

          if let content = try? NSString(contentsOf: filePath, encoding: String.Encoding.utf8.rawValue) {
            let story = Story(title: title, content: content as String)
            stories.append(story)
          }
        }
      }
    }

    DispatchQueue.main.async {
      completion(stories)
    }
  }
}
12. UIColor+Extensions.swift
import UIKit

extension UIColor {
  func colorForTranslucency() -> UIColor {
    var hue: CGFloat = 0
    var saturation: CGFloat = 0
    var brightness: CGFloat = 0
    var alpha: CGFloat = 0

    getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)

    return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha)
  }

  class func rgba(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) -> UIColor {
    return UIColor(red: red / 255.0, green: green / 255.0, blue: blue / 255.0, alpha: alpha)
  }

  class var defaultSeparator: UIColor {
    return .rgba(red: 200, green: 199, blue: 204, alpha: 1)
  }

  class var nightTimeTextBackground: UIColor {
    return .rgba(red: 245, green: 238, blue: 220, alpha: 1)
  }

  class var nightTimeText: UIColor {
    return .rgba(red: 50, green: 20, blue: 0, alpha: 1)
  }

  class var nightTimeTint: UIColor {
    return .rgba(red: 182, green: 126, blue: 44, alpha: 1)
  }
}

后记

本篇主要讲述了UIVisualEffectView原理和简单使用,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(UIKit框架(五十) —— UIVisualEffectView原理和简单使用(二))