SwiftUI框架详细解析 (十九) —— Firebase Remote Config教程(二)

版本记录

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

前言

今天翻阅苹果的API文档,发现多了一个框架SwiftUI,这里我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. SwiftUI框架详细解析 (一) —— 基本概览(一)
2. SwiftUI框架详细解析 (二) —— 基于SwiftUI的闪屏页的创建(一)
3. SwiftUI框架详细解析 (三) —— 基于SwiftUI的闪屏页的创建(二)
4. SwiftUI框架详细解析 (四) —— 使用SwiftUI进行苹果登录(一)
5. SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二)
6. SwiftUI框架详细解析 (六) —— 基于SwiftUI的导航的实现(一)
7. SwiftUI框架详细解析 (七) —— 基于SwiftUI的导航的实现(二)
8. SwiftUI框架详细解析 (八) —— 基于SwiftUI的动画的实现(一)
9. SwiftUI框架详细解析 (九) —— 基于SwiftUI的动画的实现(二)
10. SwiftUI框架详细解析 (十) —— 基于SwiftUI构建各种自定义图表(一)
11. SwiftUI框架详细解析 (十一) —— 基于SwiftUI构建各种自定义图表(二)
12. SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)
13. SwiftUI框架详细解析 (十三) —— 基于SwiftUI创建Mind-Map UI(二)
14. SwiftUI框架详细解析 (十四) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(一)
15. SwiftUI框架详细解析 (十五) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(二)
16. SwiftUI框架详细解析 (十六) —— 基于SwiftUI简单App的Dependency Injection应用(一)
17. SwiftUI框架详细解析 (十七) —— 基于SwiftUI简单App的Dependency Injection应用(二)
18. SwiftUI框架详细解析 (十八) —— Firebase Remote Config教程(一)

源码

1. Swift

首先看下工程组织结构

接着看下sb的内容

SwiftUI框架详细解析 (十九) —— Firebase Remote Config教程(二)_第1张图片

最后就看下代码

1. SolarSystem.swift
import UIKit

class SolarSystem {
  // MARK: - Properties
  static let sharedInstance = SolarSystem()

  private var planets: [Planet] = [
    Planet(
      name: "Mercury",
      yearInDays: 87.969,
      massInEarths: 0.3829,
      radiusInEarths: 0.3829,
      funFact: "The sun is trying to find a tactful way of telling Mercury it needs some personal space",
      imageName: "Mercury",
      imageCredit: "Source: NASA/Johns Hopkins University Applied Physics Laboratory/Carnegie Institution of Washington"
    ),
    Planet(
      name: "Venus",
      yearInDays: 224.701,
      massInEarths: 0.815,
      radiusInEarths: 0.9499,
      funFact: "Huge fan of saxophone solos in 80s rock songs",
      imageName: "Venus",
      imageCredit: "NASA/JPL"
    ),
    Planet(
      name: "Earth",
      yearInDays: 365.26,
      massInEarths: 1.0,
      radiusInEarths: 1.0,
      funFact: "Is it getting hot in here, or it is just me?",
      imageName: "Earth",
      imageCredit: "NASA/JPL"
    ),
    Planet(
      name: "Mars",
      yearInDays: 686.971,
      massInEarths: 0.107,
      radiusInEarths: 0.533,
      funFact: "Has selfies with Matt Damon, Arnold Schwarzenegger, The Rock",
      imageName: "Mars",
      imageCredit: """
        NASA, ESA, the Hubble Heritage Team (STScI/AURA), J. Bell (ASU), and M. Wolff (Space Science Institute)
        """
    ),
    Planet(
      name: "Jupiter",
      yearInDays: 4332.59,
      massInEarths: 317.8,
      radiusInEarths: 10.517,
      funFact: "Mortified it got a big red spot right before the Senior Planet Prom",
      imageName: "Jupiter",
      imageCredit: "NASA, ESA, and A. Simon (Goddard Space Flight Center)"
    ),
    Planet(
      name: "Saturn",
      yearInDays: 10759.22,
      massInEarths: 95.159,
      radiusInEarths: 9.449,
      funFact: "Rings consist of 80% discarded AOL CD-ROMs, 20% packing peanuts",
      imageName: "Saturn",
      imageCredit: "NASA"
    ),
    Planet(
      name: "Uranus",
      yearInDays: 30688.5,
      massInEarths: 14.536,
      radiusInEarths: 4.007,
      funFact: "Seriously, you can stop with the jokes. It's heard them all",
      imageName: "Uranus",
      imageCredit: "NASA/JPL-Caltech"
    ),
    Planet(
      name: "Neptune",
      yearInDays: 60182,
      massInEarths: 17.147,
      radiusInEarths: 3.829,
      funFact: "Claims to be a vegetarian, but eats a cheeseburger at least once a month.",
      imageName: "Neptune",
      imageCredit: "NASA"
    )
  ]
  private var shouldWeIncludePluto = true
  private var scaleFactors: [Double] = []

  private init() {
    if RCValues.sharedInstance.bool(forKey: .shouldWeIncludePluto) {
      let pluto = Planet(
        name: "Pluto",
        yearInDays: 90581,
        massInEarths: 0.002,
        radiusInEarths: 0.035,
        funFact: "Ostracized by friends for giving away too many Game of Thrones spoilers.",
        imageName: "Pluto",
        imageCredit: "NASA/JHUAPL/SwRI"
      )
      planets.append(pluto)
    }
    calculatePlanetScales()
  }

  func calculatePlanetScales() {
    // Yes, we've hard-coded Jupiter to be our largest planet. That's probably a safe assumption.
    let largestRadius = planet(at: 4).radiusInEarths
    for planet in planets {
      let ratio = planet.radiusInEarths / largestRadius
      scaleFactors.append(pow(ratio, RCValues.sharedInstance.double(forKey: .planetImageScaleFactor)))
    }
  }

  func getScaleFactor(for planetNumber: Int) -> Double {
    guard planetNumber <= scaleFactors.count else {
      return 1.0
    }

    return scaleFactors[planetNumber]
  }

  func planetCount() -> Int {
    planets.count
  }

  func planet(at number: Int) -> Planet {
    planets[number]
  }
}
2. Planet.swift
import UIKit

public struct Planet {
  // MARK: - Properties
  public let name: String
  public let yearInDays: Double
  public let massInEarths: Double
  public let radiusInEarths: Double
  public let funFact: String
  public let image: UIImage
  public let imageCredit: String

  // MARK: - Initializers
  public init(name: String, yearInDays: Double, massInEarths: Double, radiusInEarths: Double, funFact: String, imageName: String, imageCredit: String) {
    self.name = name
    self.yearInDays = yearInDays
    self.massInEarths = massInEarths
    self.radiusInEarths = radiusInEarths
    self.funFact = funFact
    self.image = UIImage(named: imageName) ?? UIImage()
    self.imageCredit = imageCredit
  }
}
3. UIColorExtension.swift
import UIKit

/// MissingHashMarkAsPrefix:   "Invalid RGB string, missing '#' as prefix"
/// UnableToScanHexValue:      "Scan hex error"
/// MismatchedHexStringLength: "Invalid RGB string, number of characters after '#' should be either 3, 4, 6 or 8"
public enum UIColorInputError: Error {
  case missingHashMarkAsPrefix
  case unableToScanHexValue
  case mismatchedHexStringLength
  case outputHexStringForWideDisplayColor
}

extension UIColor {
  /// The shorthand three-digit hexadecimal representation of color.
  /// #RGB defines to the color #RRGGBB.
  ///
  /// - parameter hex3: Three-digit hexadecimal value.
  /// - parameter alpha: 0.0 - 1.0. The default is 1.0.
  public convenience init(hex3: UInt16, alpha: CGFloat = 1) {
    let divisor = CGFloat(15)
    let red = CGFloat((hex3 & 0xF00) >> 8) / divisor
    let green = CGFloat((hex3 & 0x0F0) >> 4) / divisor
    let blue = CGFloat( hex3 & 0x00F) / divisor
    self.init(red: red, green: green, blue: blue, alpha: alpha)
  }

  /// The shorthand four-digit hexadecimal representation of color with alpha.
  /// #RGBA defines to the color #RRGGBBAA.
  ///
  /// - parameter hex4: Four-digit hexadecimal value.
  public convenience init(hex4: UInt16) {
    let divisor = CGFloat(15)
    let red = CGFloat((hex4 & 0xF000) >> 12) / divisor
    let green = CGFloat((hex4 & 0x0F00) >> 8) / divisor
    let blue = CGFloat((hex4 & 0x00F0) >> 4) / divisor
    let alpha = CGFloat( hex4 & 0x000F       ) / divisor
    self.init(red: red, green: green, blue: blue, alpha: alpha)
  }

  /// The six-digit hexadecimal representation of color of the form #RRGGBB.
  ///
  /// - parameter hex6: Six-digit hexadecimal value.
  public convenience init(hex6: UInt32, alpha: CGFloat = 1) {
    let divisor = CGFloat(255)
    let red = CGFloat((hex6 & 0xFF0000) >> 16) / divisor
    let green = CGFloat((hex6 & 0x00FF00) >> 8) / divisor
    let blue = CGFloat( hex6 & 0x0000FF       ) / divisor
    self.init(red: red, green: green, blue: blue, alpha: alpha)
  }

  /// The six-digit hexadecimal representation of color with alpha of the form #RRGGBBAA.
  ///
  /// - parameter hex8: Eight-digit hexadecimal value.
  public convenience init(hex8: UInt32) {
    let divisor = CGFloat(255)
    let red = CGFloat((hex8 & 0xFF000000) >> 24) / divisor
    let green = CGFloat((hex8 & 0x00FF0000) >> 16) / divisor
    let blue = CGFloat((hex8 & 0x0000FF00) >> 8) / divisor
    let alpha = CGFloat( hex8 & 0x000000FF       ) / divisor
    self.init(red: red, green: green, blue: blue, alpha: alpha)
  }

  /// The rgba string representation of color with alpha of the form #RRGGBBAA/#RRGGBB, throws error.
  ///
  /// - parameter rgba: String value.
  public convenience init(rgbaThrows rgba: String) throws {
    guard rgba.hasPrefix("#") else {
      throw UIColorInputError.missingHashMarkAsPrefix
    }

    let hexString = String(rgba[String.Index(utf16Offset: 1, in: rgba)...])
    var hexValue: UInt32 = 0

    guard Scanner(string: hexString).scanHexInt32(&hexValue) else {
      throw UIColorInputError.unableToScanHexValue
    }

    switch hexString.count {
    case 3:
      self.init(hex3: UInt16(hexValue))
    case 4:
      self.init(hex4: UInt16(hexValue))
    case 6:
      self.init(hex6: hexValue)
    case 8:
      self.init(hex8: hexValue)
    default:
      throw UIColorInputError.mismatchedHexStringLength
    }
  }

  /// The rgba string representation of color with alpha of the form #RRGGBBAA/#RRGGBB, fails to default color.
  ///
  /// - parameter rgba: String value.
  public convenience init(_ rgba: String, defaultColor: UIColor = UIColor.clear) {
    guard let color = try? UIColor(rgbaThrows: rgba) else {
      self.init(cgColor: defaultColor.cgColor)
      return
    }

    self.init(cgColor: color.cgColor)
  }

  /// Hex string of a UIColor instance, throws error.
  ///
  /// - parameter includeAlpha: Whether the alpha should be included.
  public func hexStringThrows(_ includeAlpha: Bool = true) throws -> String {
    var red: CGFloat = 0
    var green: CGFloat = 0
    var blue: CGFloat = 0
    var alpha: CGFloat = 0
    self.getRed(&red, green: &green, blue: &blue, alpha: &alpha)

    guard red >= 0 && red <= 1 && green >= 0 && green <= 1 && blue >= 0 && blue <= 1 else {
      throw UIColorInputError.outputHexStringForWideDisplayColor
    }

    if includeAlpha {
      return String(format: "#%02X%02X%02X%02X", Int(red * 255), Int(green * 255), Int(blue * 255), Int(alpha * 255))
    } else {
      return String(format: "#%02X%02X%02X", Int(red * 255), Int(green * 255), Int(blue * 255))
    }
  }

  /// Hex string of a UIColor instance, fails to empty string.
  ///
  /// - parameter includeAlpha: Whether the alpha should be included.
  public func hexString(_ includeAlpha: Bool = true) -> String {
    guard let hexString = try? hexStringThrows(includeAlpha) else {
      return ""
    }

    return hexString
  }
}

extension String {
  /// Convert argb string to rgba string.
  public func argb2rgba() -> String? {
    guard self.hasPrefix("#") else {
      return nil
    }

    let hexString = String(self[self.index(self.startIndex, offsetBy: 1)...])
    switch hexString.count {
    case 4:
      let firstHalf = String(hexString[self.index(self.startIndex, offsetBy: 1)...])
      let secondHalf = String(hexString[..
4. CrossfadeSegue.swift
import UIKit

class CrossfadeSegue: UIStoryboardSegue {
  override func perform() {
    let secondVCView = destination.view
    secondVCView?.alpha = 0.0
    source.navigationController?.pushViewController(destination, animated: false)
    UIView.animate(withDuration: 0.4) {
      secondVCView?.alpha = 1.0
    }
  }
}
5. PlanetaryCollectionViewFlowLayout.swift
import UIKit

class PlanetaryCollectionViewFlowLayout: UICollectionViewFlowLayout {
  // MARK: - Properties
  let topSpacing: CGFloat = 80
  let betweenSpacing: CGFloat = 10

  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    guard
      let superAttributes = super.layoutAttributesForElements(in: rect),
      let attributesToReturn = NSArray(
        array: superAttributes, copyItems: true
      ) as? [UICollectionViewLayoutAttributes]
    else {
      return nil
    }

    for attribute in attributesToReturn where attribute.representedElementKind == nil {
      guard let itemLayoutAttributes = layoutAttributesForItem(at: attribute.indexPath) else {
        continue
      }

      attribute.frame = itemLayoutAttributes.frame
    }

    return attributesToReturn
  }

  // This gives us a top-aligned horizontal layout
  override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    guard
      let superItemAttributes = super.layoutAttributesForItem(at: indexPath),
      let currentItemAttributes = superItemAttributes.copy() as? UICollectionViewLayoutAttributes,
      let collectionView = collectionView,
      let sectionInset = (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionInset
    else {
      return nil
    }

    if indexPath.item == 0 {
      var frame = currentItemAttributes.frame
      frame.origin.y = sectionInset.top + topSpacing
      currentItemAttributes.frame = frame
      return currentItemAttributes
    }

    let previousIndexPath = IndexPath(item: indexPath.item - 1, section: indexPath.section)
    guard let previousFrame = layoutAttributesForItem(at: previousIndexPath)?.frame else {
      return nil
    }

    let previousFrameRightPoint = previousFrame.origin.y + previousFrame.size.height + betweenSpacing
    let previousFrameTop = previousFrame.origin.y
    let currentFrame = currentItemAttributes.frame
    let stretchedCurrentFrame = CGRect(
      x: currentFrame.origin.x,
      y: previousFrameTop,
      width: currentFrame.size.width,
      height: collectionView.frame.size.height
    )
    if !previousFrame.intersects(stretchedCurrentFrame) {
      var frame = currentItemAttributes.frame
      frame.origin.y = sectionInset.top + topSpacing
      currentItemAttributes.frame = frame
      return currentItemAttributes
    }

    var frame = currentItemAttributes.frame
    frame.origin.y = previousFrameRightPoint
    currentItemAttributes.frame = frame
    return currentItemAttributes
  }

  // This controlls the scrolling of the collection view so that it comes to rest with the closest
  // planet on the center of the screen
  override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    guard let collectionView = collectionView else {
      return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
    }

    let collectionViewBounds = collectionView.bounds
    let halfWidth = collectionViewBounds.size.width * 0.5
    let proposedContentOffsetCenterX = proposedContentOffset.x + halfWidth

    guard
      let attributesForVisibleCells = layoutAttributesForElements(
        in: collectionViewBounds
      ) as [UICollectionViewLayoutAttributes]?,
      let closestAttribute = attributesForVisibleCells.reduce(nil, { closest, nextAttribute in
        getClosestAttribute(closest, nextAttribute: nextAttribute, targetCenterX: proposedContentOffsetCenterX)
      })
    else {
      return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
    }

    return CGPoint(x: closestAttribute.center.x - halfWidth, y: proposedContentOffset.y)
  }

  func getClosestAttribute(_ closestSoFar: UICollectionViewLayoutAttributes?, nextAttribute: UICollectionViewLayoutAttributes, targetCenterX: CGFloat) -> UICollectionViewLayoutAttributes? {
    if
      let closestSoFar = closestSoFar,
      abs(nextAttribute.center.x - targetCenterX) < abs(closestSoFar.center.x - targetCenterX)
    {
      return nextAttribute
    } else if let closestSoFar = closestSoFar {
      return closestSoFar
    }

    return nextAttribute
  }
}
6. RCValues.swift
import Foundation
import Firebase

enum ValueKey: String {
  case bigLabelColor
  case appPrimaryColor
  case navBarBackground
  case navTintColor
  case detailTitleColor
  case detailInfoColor
  case subscribeBannerText
  case subscribeBannerButton
  case subscribeVCText
  case subscribeVCButton
  case shouldWeIncludePluto
  case experimentGroup
  case planetImageScaleFactor
}

class RCValues {
  static let sharedInstance = RCValues()
  var loadingDoneCallback: (() -> Void)?
  var fetchComplete = false

  private init() {
    loadDefaultValues()
    fetchCloudValues()
  }

  func loadDefaultValues() {
    let appDefaults: [String: Any?] = [
      ValueKey.bigLabelColor.rawValue: "#FFFFFF66",
      ValueKey.appPrimaryColor.rawValue: "#FBB03B",
      ValueKey.navBarBackground.rawValue: "#535E66",
      ValueKey.navTintColor.rawValue: "#FBB03B",
      ValueKey.detailTitleColor.rawValue: "#FFFFFF",
      ValueKey.detailInfoColor.rawValue: "#CCCCCC",
      ValueKey.subscribeBannerText.rawValue: "Like PlanetTour?",
      ValueKey.subscribeBannerButton.rawValue: "Get our newsletter!",
      ValueKey.subscribeVCText.rawValue: "Want more astronomy facts? Sign up for our newsletter!",
      ValueKey.subscribeVCButton.rawValue: "Subscribe",
      ValueKey.shouldWeIncludePluto.rawValue: false,
      ValueKey.experimentGroup.rawValue: "default",
      ValueKey.planetImageScaleFactor.rawValue: 0.33
    ]
    RemoteConfig.remoteConfig().setDefaults(appDefaults as? [String: NSObject])
  }

  func fetchCloudValues() {
    activateDebugMode()

    RemoteConfig.remoteConfig().fetch { [weak self] _, error in
      if let error = error {
        print("Uh-oh. Got an error fetching remote values \(error)")
        // In a real app, you would probably want to call the loading done callback anyway,
        // and just proceed with the default values. I won't do that here, so we can call attention
        // to the fact that Remote Config isn't loading.
        return
      }

      RemoteConfig.remoteConfig().activate { [weak self] _, _ in
        print("Retrieved values from the cloud!")
        self?.fetchComplete = true
        DispatchQueue.main.async {
          self?.loadingDoneCallback?()
        }
      }
    }
  }

  func activateDebugMode() {
    let settings = RemoteConfigSettings()
    // WARNING: Don't actually do this in production!
    settings.minimumFetchInterval = 0
    RemoteConfig.remoteConfig().configSettings = settings
  }

  func color(forKey key: ValueKey) -> UIColor {
    let colorAsHexString = RemoteConfig.remoteConfig()[key.rawValue].stringValue ?? "#FFFFFFFF"
    let convertedColor = UIColor(colorAsHexString)
    return convertedColor
  }

  func bool(forKey key: ValueKey) -> Bool {
    RemoteConfig.remoteConfig()[key.rawValue].boolValue
  }

  func string(forKey key: ValueKey) -> String {
    RemoteConfig.remoteConfig()[key.rawValue].stringValue ?? ""
  }

  func double(forKey key: ValueKey) -> Double {
    RemoteConfig.remoteConfig()[key.rawValue].numberValue.doubleValue
  }
}
7. PlanetsCollectionViewController.swift
import UIKit

class PlanetsCollectionViewController: UICollectionViewController {
  // MARK: - Properties
  private let reuseIdentifier = "PlanetCell"
  private let sectionInsets = UIEdgeInsets(top: 10, left: 80, bottom: 10, right: 70)
  var starBackground: UIImageView?
  var systemMap: MiniMap?

  // MARK: - View Life Cycle
  override func viewDidLoad() {
    super.viewDidLoad()

    collectionView?.backgroundColor = UIColor(white: 0, alpha: 0.6)
    collectionView?.contentInsetAdjustmentBehavior = .automatic
  }

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

    customizeNavigationBar()
  }

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

    removeWaitingViewController()
  }

  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    addFancyBackground()
    addMiniMap()
  }

  // MARK: - Navigation
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard
      let planetDetail = segue.destination as? PlanetDetailViewController,
      let firstIndexPath = collectionView?.indexPathsForSelectedItems?.first
    else {
      return
    }

    let selectedPlanetNumber = firstIndexPath.row
    planetDetail.planet = SolarSystem.sharedInstance.planet(at: selectedPlanetNumber)
  }

  override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
    super.willTransition(to: newCollection, with: coordinator)

    collectionView?.collectionViewLayout.invalidateLayout()
  }
}

// MARK: - Internal
extension PlanetsCollectionViewController {
  func addFancyBackground() {
    guard
      starBackground == nil,
      let galaxyImage = UIImage(named: "GalaxyBackground")
    else {
      return
    }

    starBackground = UIImageView(image: galaxyImage)
    let scaleFactor = view.bounds.height / galaxyImage.size.height
    starBackground?.frame = CGRect(
      x: 0,
      y: 0,
      width: galaxyImage.size.width * scaleFactor,
      height: galaxyImage.size.height * scaleFactor
    )
    view.insertSubview(starBackground ?? UIImageView(), at: 0)
  }

  func addMiniMap() {
    guard systemMap == nil else {
      return
    }

    let miniMapFrame = CGRect(
      x: view.bounds.width * 0.1,
      y: view.bounds.height - 80,
      width: view.bounds.width * 0.8,
      height: 40
    )
    systemMap = MiniMap(frame: miniMapFrame)
    view.addSubview(systemMap ?? MiniMap())
  }

  func customizeNavigationBar() {
    guard let navBar = navigationController?.navigationBar else {
      return
    }

    navBar.barTintColor = RCValues.sharedInstance.color(forKey: .navBarBackground)
    let targetFont = UIFont(name: "Avenir-black", size: 18.0) ?? UIFont.systemFont(ofSize: 18.0)
    navBar.titleTextAttributes = [
      NSAttributedString.Key.foregroundColor: UIColor.white,
      NSAttributedString.Key.font: targetFont
    ]
  }

  func removeWaitingViewController() {
    guard
      let stackViewControllers = navigationController?.viewControllers,
      stackViewControllers.first is WaitingViewController
    else {
      return
    }

    navigationController?.viewControllers.remove(at: 0)
  }
}

// MARK: - UICollectionViewDataSource
extension PlanetsCollectionViewController {
  override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return SolarSystem.sharedInstance.planetCount()
  }

  func getImageSize(for planetNum: Int, withWidth: CGFloat) -> CGFloat {
    let scaleFactor = SolarSystem.sharedInstance.getScaleFactor(for: planetNum)
    return withWidth * CGFloat(scaleFactor)
  }

  override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let cell = collectionView.dequeueReusableCell(
      withReuseIdentifier: reuseIdentifier,
      for: indexPath
    ) as? PlanetCell
    else {
      return collectionView.dequeueReusableCell(
        withReuseIdentifier: reuseIdentifier,
        for: indexPath
      )
    }

    let currentPlanet = SolarSystem.sharedInstance.planet(at: indexPath.row)
    let planetImageSize = getImageSize(for: indexPath.row, withWidth: cell.bounds.width)
    cell.imageView.image = currentPlanet.image
    cell.imageWidth.constant = planetImageSize
    cell.imageHeight.constant = planetImageSize
    cell.nameLabel.text = currentPlanet.name
    cell.nameLabel.textColor = RCValues.sharedInstance.color(forKey: .bigLabelColor)
    return cell
  }
}

// MARK: - UICollectionViewDelegate
extension PlanetsCollectionViewController {
  override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    performSegue(withIdentifier: "planetDetailSegue", sender: self)
  }
}

// MARK: - UIScrollViewDelegate
extension PlanetsCollectionViewController {
  override func scrollViewDidScroll(_ scrollView: UIScrollView) {
    guard let collectionView = collectionView else {
      return
    }

    // Parallax scrolling
    let pctThere: CGFloat = scrollView.contentOffset.x / scrollView.contentSize.width
    let backgroundTravel: CGFloat = (starBackground?.frame.width ?? 0) - view.frame.width
    starBackground?.frame.origin = CGPoint(x: -pctThere * backgroundTravel, y: 0)

    // Adjust the mini-map
    let centerX: CGFloat = collectionView.contentOffset.x + (collectionView.bounds.width * 0.5)
    let centerPoint = CGPoint(x: centerX, y: collectionView.bounds.height * 0.5)
    guard let visibleIndexPath = collectionView.indexPathForItem(at: centerPoint) else {
      return
    }

    systemMap?.showPlanet(number: visibleIndexPath.item)
  }
}

// MARK: - UICollectionViewDelegateFlowLayout
extension PlanetsCollectionViewController: UICollectionViewDelegateFlowLayout {
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let cellHeight = biggestSizeThatFits()
    let cellWidth = max(0.5, CGFloat(SolarSystem.sharedInstance.getScaleFactor(for: indexPath.row))) * cellHeight
    return CGSize(width: cellWidth, height: cellHeight)
  }

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    return sectionInsets
  }

  private func biggestSizeThatFits() -> CGFloat {
    let maxHeight = view.frame.height - sectionInsets.top - sectionInsets.bottom - 150
    let idealCellSize = CGFloat(380)
    let cellSize = min(maxHeight, idealCellSize)
    return cellSize
  }
}
8. GetNewsletterViewController.swift
import UIKit

class GetNewsletterViewController: UIViewController {
  // MARK: - IBOutlets
  @IBOutlet weak var instructionLabel: UILabel!
  @IBOutlet weak var thankYouLabel: UILabel!
  @IBOutlet weak var submitButton: UIButton!
  @IBOutlet weak var emailTextField: UITextField!

  // MARK: - View Life Cycle
  override func viewDidLoad() {
    super.viewDidLoad()

    updateText()
    updateSubmitButton()
    thankYouLabel.isHidden = true
  }
}

// MARK: - IBActions
extension GetNewsletterViewController {
  @IBAction func submitButtonWasPressed(_ sender: AnyObject) {
    // We won't actually submit an email, but we can pretend
    submitButton.isHidden = true
    thankYouLabel.isHidden = false
    emailTextField.isEnabled = false
  }
}

// MARK: - Private
private extension GetNewsletterViewController {
  func updateText() {
    instructionLabel.text = RCValues.sharedInstance.string(forKey: .subscribeVCText)
    submitButton.setTitle(RCValues.sharedInstance.string(forKey: .subscribeVCButton), for: .normal)
  }

  func updateSubmitButton() {
    submitButton.backgroundColor = RCValues.sharedInstance.color(forKey: .appPrimaryColor)
    submitButton.layer.cornerRadius = 5.0
  }
}
9. ContainerViewController.swift
import UIKit

class ContainerViewController: UIViewController {
  // MARK: - IBOutlets
  @IBOutlet weak var bannerView: UIView!
  @IBOutlet weak var bannerLabel: UILabel!
  @IBOutlet weak var getNewsletterButton: UIButton!

  // MARK: - View Life Cycle
  override func viewDidLoad() {
    super.viewDidLoad()
    updateBanner()
  }

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

    updateNavigationColors()
  }
}

// MARK: - Private
private extension ContainerViewController {
  func updateNavigationColors() {
    navigationController?.navigationBar.tintColor = RCValues.sharedInstance.color(forKey: .navTintColor)
  }

  func updateBanner() {
    bannerView.backgroundColor = RCValues.sharedInstance.color(forKey: .appPrimaryColor)
    bannerLabel.text = RCValues.sharedInstance.string(forKey: .subscribeBannerText)
    getNewsletterButton.setTitle(RCValues.sharedInstance.string(forKey: .subscribeBannerButton), for: .normal)
  }
}

// MARK: - IBActions
extension ContainerViewController {
  @IBAction func getNewsletterButtonWasPressed(_ sender: AnyObject) {
    // No-op right now.
  }
}
10. PlanetDetailViewController.swift
import UIKit

class PlanetDetailViewController: UIViewController {
  // MARK: - IBOutlets
  @IBOutlet weak var planetNameLabel: UILabel!
  @IBOutlet weak var planetImage: UIImageView!
  @IBOutlet weak var yearLengthLabel: UILabel!
  @IBOutlet weak var massTitle: UILabel!
  @IBOutlet weak var yearTitle: UILabel!
  @IBOutlet weak var funFactTitle: UILabel!
  @IBOutlet weak var massLabel: UILabel!
  @IBOutlet weak var funFactLabel: UILabel!
  @IBOutlet weak var imageCreditLabel: UILabel!

  // MARK: - Properties
  var planet: Planet?

  // MARK: - View Life Cycle
  override func viewDidLoad() {
    super.viewDidLoad()
    updateLabelColors()
    updateLookForPlanet()
  }
}

// MARK: - Private
private extension PlanetDetailViewController {
  func updateLabelColors() {
    for case let nextLabel? in [yearTitle, massTitle, funFactTitle] {
      nextLabel.textColor = RCValues.sharedInstance.color(forKey: .appPrimaryColor)
    }

    for case let nextLabel? in [yearLengthLabel, massLabel, funFactLabel] {
      nextLabel.textColor = RCValues.sharedInstance.color(forKey: .detailInfoColor)
    }

    planetNameLabel.textColor = RCValues.sharedInstance.color(forKey: .detailTitleColor)
  }

  func updateLookForPlanet() {
    guard let planet = planet else {
      return
    }

    planetNameLabel.text = planet.name
    planetImage.image = planet.image
    yearLengthLabel.text = String(planet.yearInDays)
    massLabel.text = String(planet.massInEarths)
    funFactLabel.text = planet.funFact
    imageCreditLabel.text = "Image credit: \(planet.imageCredit)"
  }
}
11. WaitingViewController.swift
import UIKit

class WaitingViewController: UIViewController {
  @IBOutlet weak var justAMomentLabel: UILabel!

  // MARK: - View Life Cycle
  override func viewDidLoad() {
    super.viewDidLoad()

    if RCValues.sharedInstance.fetchComplete {
      startAppForReal()
    }

    RCValues.sharedInstance.loadingDoneCallback = startAppForReal
  }

  func startAppForReal() {
    performSegue(withIdentifier: "loadingDoneSegue", sender: self)
  }
}
12. PlanetCell.swift
import UIKit

class PlanetCell: UICollectionViewCell {
  // MARK: - IBOutlets
  @IBOutlet weak var imageView: UIImageView!
  @IBOutlet weak var nameLabel: UILabel!
  @IBOutlet weak var imageHeight: NSLayoutConstraint!
  @IBOutlet weak var imageWidth: NSLayoutConstraint!
}
13. MiniMap.swift
import UIKit

class MiniMap: UIView {
  // MARK: - Properties
  var mapImage = UIImageView()
  var overviewImage = UIImageView()
  var frameRects: [CGRect] = []
  let originalFrameBasis: CGFloat = 600
  var oldPlanet: Int = -1

  // MARK: - Initializers
  override init(frame: CGRect) {
    super.init(frame: frame)

    frameRects = [
      CGRect(x: 21, y: 48, width: 27, height: 31),
      CGRect(x: 53, y: 47, width: 30, height: 30),
      CGRect(x: 97, y: 47, width: 30, height: 30),
      CGRect(x: 142, y: 52, width: 20, height: 20),
      CGRect(x: 174, y: 11, width: 105, height: 102),
      CGRect(x: 283, y: 5, width: 160, height: 107),
      CGRect(x: 427, y: 39, width: 45, height: 49),
      CGRect(x: 484, y: 40, width: 46, height: 46),
      CGRect(x: 547, y: 53, width: 17, height: 17)
    ]
    createMapImage()
    createOverviewImage()
  }

  @available(*, unavailable)
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  func createMapImage() {
    mapImage = UIImageView(image: UIImage(named: "SolarSystem"))
    mapImage.contentMode = .scaleAspectFit
    addSubview(mapImage)
  }

  func createOverviewImage() {
    let frameInsets = UIEdgeInsets(top: 5.0, left: 5.0, bottom: 5.0, right: 5.0)
    overviewImage = UIImageView(image: UIImage(named: "PlanetFrame")?.resizableImage(withCapInsets: frameInsets))
    addSubview(overviewImage)
    showPlanet(number: 0)
  }

  func showPlanet(number planetNum: Int) {
    guard planetNum != oldPlanet else {
      return
    }

    oldPlanet = planetNum
    let normalRect = frameRects[planetNum]
    let multiplier = mapImage.bounds.width / originalFrameBasis
    let destinationRect = CGRect(
      x: normalRect.origin.x * multiplier,
      y: normalRect.origin.y * multiplier,
      width: normalRect.width * multiplier,
      height: normalRect.height * multiplier
    )
    UIView.animate(withDuration: 0.3, delay: 0.0) {
      self.overviewImage.frame = destinationRect
    }
  }
}

后记

本篇主要讲述了Firebase Remote Config教程,感兴趣的给个赞或者关注~~~

SwiftUI框架详细解析 (十九) —— Firebase Remote Config教程(二)_第2张图片

你可能感兴趣的:(SwiftUI框架详细解析 (十九) —— Firebase Remote Config教程(二))