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