版本記錄
| 版本號(hào) | 時(shí)間 |
|---|---|
| V1.0 | 2021.01.03 星期日 |
前言
今天翻閱蘋(píng)果的API文檔,發(fā)現(xiàn)多了一個(gè)框架SwiftUI,這里我們就一起來(lái)看一下這個(gè)框架。感興趣的看下面幾篇文章。
1. SwiftUI框架詳細(xì)解析 (一) —— 基本概覽(一)
2. SwiftUI框架詳細(xì)解析 (二) —— 基于SwiftUI的閃屏頁(yè)的創(chuàng)建(一)
3. SwiftUI框架詳細(xì)解析 (三) —— 基于SwiftUI的閃屏頁(yè)的創(chuàng)建(二)
4. SwiftUI框架詳細(xì)解析 (四) —— 使用SwiftUI進(jìn)行蘋(píng)果登錄(一)
5. SwiftUI框架詳細(xì)解析 (五) —— 使用SwiftUI進(jìn)行蘋(píng)果登錄(二)
6. SwiftUI框架詳細(xì)解析 (六) —— 基于SwiftUI的導(dǎo)航的實(shí)現(xiàn)(一)
7. SwiftUI框架詳細(xì)解析 (七) —— 基于SwiftUI的導(dǎo)航的實(shí)現(xiàn)(二)
8. SwiftUI框架詳細(xì)解析 (八) —— 基于SwiftUI的動(dòng)畫(huà)的實(shí)現(xiàn)(一)
9. SwiftUI框架詳細(xì)解析 (九) —— 基于SwiftUI的動(dòng)畫(huà)的實(shí)現(xiàn)(二)
10. SwiftUI框架詳細(xì)解析 (十) —— 基于SwiftUI構(gòu)建各種自定義圖表(一)
11. SwiftUI框架詳細(xì)解析 (十一) —— 基于SwiftUI構(gòu)建各種自定義圖表(二)
12. SwiftUI框架詳細(xì)解析 (十二) —— 基于SwiftUI創(chuàng)建Mind-Map UI(一)
13. SwiftUI框架詳細(xì)解析 (十三) —— 基于SwiftUI創(chuàng)建Mind-Map UI(二)
14. SwiftUI框架詳細(xì)解析 (十四) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(一)
15. SwiftUI框架詳細(xì)解析 (十五) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(二)
16. SwiftUI框架詳細(xì)解析 (十六) —— 基于SwiftUI簡(jiǎn)單App的Dependency Injection應(yīng)用(一)
17. SwiftUI框架詳細(xì)解析 (十七) —— 基于SwiftUI簡(jiǎn)單App的Dependency Injection應(yīng)用(二)
18. SwiftUI框架詳細(xì)解析 (十八) —— Firebase Remote Config教程(一)
源碼
1. Swift
首先看下工程組織結(jié)構(gòu)

接著看下sb的內(nèi)容

最后就看下代碼
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教程,感興趣的給個(gè)贊或者關(guān)注~~~
