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的内容

最后就看下代码

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[..<self.index(self.startIndex, offsetBy: 1)])
      return "#\(firstHalf)\(secondHalf)"
    case 8:
      let firstHalf = String(hexString[self.index(self.startIndex, offsetBy: 2)...])
      let secondHalf = String(hexString[..<self.index(self.startIndex, offsetBy: 2)])
      return "#\(firstHalf)\(secondHalf)"
    default:
      return nil
    }
  }
}
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教程,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容