美文网首页js css html
SwiftUI框架详细解析 (十九) —— Firebase R

SwiftUI框架详细解析 (十九) —— Firebase R

作者: 刀客传奇 | 来源:发表于2021-01-03 10:13 被阅读0次

    版本记录

    版本号 时间
    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教程,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

        本文标题:SwiftUI框架详细解析 (十九) —— Firebase R

        本文链接:https://www.haomeiwen.com/subject/mtieoktx.html