SF Symbols詳細介紹(三) —— 簡單使用介紹(二) 版本記錄 前言 源碼 後記

版本記錄

版本號 時間
V1.0 2021.05.23 星期日

前言

SF Symbols 在 WWDC 2019 期間推出。自此Apple 爲我們提供了免費 Symbols,供我們在應用中使用,而且使用它們非常簡單。 不久前,WWDC 2020 又引入了 SF Symbols 2.0,這讓我們在 app 中使用精美的圖標更加容易。感興趣的可以看下面幾篇文章。
1. SF Symbols詳細介紹(一) —— 簡介(一)
2. SF Symbols詳細介紹(二) —— 簡單使用介紹(一)

源碼

1. Swift

首先看下工程組織結構。

下面就是源碼啦

1. AppMain.swift
import SwiftUI

@main
struct AppMain: App {
  var body: some Scene {
    WindowGroup {
      TubeStatusView(
        model: TubeStatusViewModel(tubeLinesStatusFetcher: TubeLinesStatusFetcherFactory.new())
      )
    }
  }
}
2. UIColor.swift
import SwiftUI

extension Color {
  var luminance: CGFloat {
    var red: CGFloat = 0
    var green: CGFloat = 0
    var blue: CGFloat = 0

    guard let cgColor = cgColor else {
      return 0
    }
    let color = UIColor(cgColor: cgColor)
    color.getRed(&red, green: &green, blue: &blue, alpha: nil)

    return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue)
  }

  var isLight: Bool {
    return luminance >= 0.5
  }

  var contrastingTextColor: Color {
    if isLight {
      return Color.black
    } else {
      return Color.white
    }
  }

  static var tflBlue = Color(red: 17 / 255, green: 59 / 255, blue: 146 / 255)
}
3. DebugLineData.swift
import Foundation
import SwiftUI

let bakerlooLineDebug = LineData(name: "BakerlooDebug", color: Color(red: 137 / 255, green: 78 / 255, blue: 36 / 255))
let centralLineDebug = LineData(name: "CentralDebug", color: Color(red: 220 / 255, green: 36 / 255, blue: 31 / 255))
let circleLineDebug = LineData(name: "CircleDebug", color: Color(red: 255 / 255, green: 206 / 255, blue: 0 / 255))
let districtLineDebug = LineData(name: "DistrictDebug", color: Color(red: 0 / 255, green: 114 / 255, blue: 41 / 255))
let hammersmithAndCityLineDebug = LineData(
  name: "Hammersmith & CityDebug",
  color: Color(red: 215 / 255, green: 153 / 255, blue: 175 / 255)
)
let jubileeLineDebug = LineData(name: "JubileeDebug", color: Color(red: 106 / 255, green: 114 / 255, blue: 120 / 255))
let metropolitanLineDebug = LineData(
  name: "MetropolitanDebug", color: Color(red: 117 / 255, green: 16 / 255, blue: 86 / 255)
)
let northernLineDebug = LineData(name: "NorthernDebug", color: Color(red: 0 / 255, green: 0 / 255, blue: 0 / 255))
let piccadillyLineDebug = LineData(
  name: "PiccadillyDebug", color: Color(red: 0 / 255, green: 25 / 255, blue: 168 / 255)
)
let victoriaLineDebug = LineData(
  name: "VictoriaDebug",
  color: Color(red: 0 / 255, green: 160 / 255, blue: 226 / 255)
)

let debugData = AllLinesStatus(
  lastUpdated: Date(),
  linesStatus: [
    LineStatus(line: bakerlooLine, status: .specialService),
    LineStatus(line: centralLine, status: .closed),
    LineStatus(line: circleLine, status: .suspended),
    LineStatus(line: districtLine, status: .partSuspended),
    LineStatus(line: hammersmithAndCityLine, status: .plannedClosure),
    LineStatus(line: jubileeLine, status: .partClosure),
    LineStatus(line: metropolitanLine, status: .severeDelays),
    LineStatus(line: northernLine, status: .reducedService),
    LineStatus(line: piccadillyLine, status: .busService),
    LineStatus(line: victoriaLine, status: .minorDelays),
    LineStatus(line: waterlooAndCityLine, status: .goodService),
    LineStatus(line: dlr, status: .partClosed),
    LineStatus(line: bakerlooLineDebug, status: .exitOnly),
    LineStatus(line: centralLineDebug, status: .noStepFreeAccess),
    LineStatus(line: circleLineDebug, status: .changeOfFrequency),
    LineStatus(line: districtLineDebug, status: .diverted),
    LineStatus(line: hammersmithAndCityLineDebug, status: .notRunning),
    LineStatus(line: jubileeLineDebug, status: .issuesReported),
    LineStatus(line: metropolitanLineDebug, status: .noIssues),
    LineStatus(line: northernLineDebug, status: .information),
    LineStatus(line: piccadillyLineDebug, status: .serviceClosed),
    LineStatus(line: victoriaLineDebug, status: .unknown)
  ]
)
4. DebugDataService.swift
import Combine

final class DebugDataService: TubeLinesStatusFetcher {
  func fetchStatus() -> Future<AllLinesStatus, Error> {
    return Future { promise in
      promise(.success(debugData))
    }
  }
}
5. LineData.swift
import SwiftUI

// Static line data that doesn't change (Name, color)
struct LineData {
  let name: String
  let color: Color
}

let bakerlooLine = LineData(name: "Bakerloo", color: Color(red: 137 / 255, green: 78 / 255, blue: 36 / 255))
let centralLine = LineData(name: "Central", color: Color(red: 220 / 255, green: 36 / 255, blue: 31 / 255))
let circleLine = LineData(name: "Circle", color: Color(red: 255 / 255, green: 206 / 255, blue: 0 / 255))
let districtLine = LineData(name: "District", color: Color(red: 0 / 255, green: 114 / 255, blue: 41 / 255))
let hammersmithAndCityLine = LineData(
  name: "Hammersmith & City",
  color: Color(red: 215 / 255, green: 153 / 255, blue: 175 / 255)
)
let jubileeLine = LineData(name: "Jubilee", color: Color(red: 106 / 255, green: 114 / 255, blue: 120 / 255))
let metropolitanLine = LineData(name: "Metropolitan", color: Color(red: 117 / 255, green: 16 / 255, blue: 86 / 255))
let northernLine = LineData(name: "Northern", color: Color(red: 0 / 255, green: 0 / 255, blue: 0 / 255))
let piccadillyLine = LineData(name: "Piccadilly", color: Color(red: 0 / 255, green: 25 / 255, blue: 168 / 255))
let victoriaLine = LineData(name: "Victoria", color: Color(red: 0 / 255, green: 160 / 255, blue: 226 / 255))
let waterlooAndCityLine = LineData(
  name: "Waterloo & City Line", color: Color(red: 118 / 255, green: 208 / 255, blue: 189 / 255)
)
let dlr = LineData(name: "DLR", color: Color(red: 0 / 255, green: 175 / 255, blue: 173 / 255))
6. TFLLineStatus.swift
import Foundation
import SwiftUI

enum TFLLineStatus: Int, CaseIterable {
  case unknown = -1
  case specialService
  case closed
  case suspended
  case partSuspended
  case plannedClosure
  case partClosure
  case severeDelays
  case reducedService
  case busService
  case minorDelays
  case goodService
  case partClosed
  case exitOnly
  case noStepFreeAccess
  case changeOfFrequency
  case diverted
  case notRunning
  case issuesReported
  case noIssues
  case information
  case serviceClosed

  // swiftlint:disable:next cyclomatic_complexity
  func displayName() -> String {
    switch self {
    case .specialService:
      return "Special Service"
    case .closed:
      return "Closed"
    case .suspended:
      return "Suspended"
    case .partSuspended:
      return "Part Suspended"
    case .plannedClosure:
      return "Planned Closure"
    case .partClosure:
      return "Part Closure"
    case .severeDelays:
      return "Severe Delays"
    case .reducedService:
      return "Reduced Service"
    case .busService:
      return "Bus Service"
    case .minorDelays:
      return "Minor Delays"
    case .goodService:
      return "Good Service"
    case .partClosed:
      return "Part Closed"
    case .exitOnly:
      return "Exit Only"
    case .noStepFreeAccess:
      return "No Step Free Access"
    case .changeOfFrequency:
      return "Change of Frequency"
    case .diverted:
      return "Diverted"
    case .notRunning:
      return "Not Running"
    case .issuesReported:
      return "Issues Reported"
    case .noIssues:
      return "No Issues"
    case .information:
      return "Information"
    case .serviceClosed:
      return "Service Closed"
    case .unknown:
      return "Unknown"
    }
  }

  // swiftlint:disable:next cyclomatic_complexity
  func image() -> Image {
    switch self {
    case .closed:
      return Image(systemName: "exclamationmark.octagon")
    case .suspended:
      return Image(systemName: "nosign")
    case .severeDelays:
      return Image(systemName: "exclamationmark.arrow.circlepath")
    case .reducedService:
      return Image(systemName: "tortoise")
    case .busService:
      return Image(systemName: "bus")
    case .minorDelays:
      return Image(systemName: "clock.arrow.circlepath")
    case .goodService:
      return Image(systemName: "checkmark.square")
    case .changeOfFrequency:
      return Image(systemName: "clock.arrow.2.circlepath")
    case .notRunning:
      return Image(systemName: "exclamationmark.octagon")
    case .issuesReported:
      return Image(systemName: "exclamationmark.circle")
    case .noIssues:
      return Image(systemName: "checkmark.square")
    case .plannedClosure:
      return Image(systemName: "hammer")
    case .serviceClosed:
      return Image(systemName: "exclamationmark.octagon")
    case .unknown:
      return Image(systemName: "questionmark.circle")
    case .specialService:
      return Image("special.service")
    case .partSuspended:
      return Image("part.suspended")
    case .partClosure:
      return Image("part.closure")
    case .partClosed:
      return Image("part.closure")
    case .exitOnly:
      return Image("exit.only")
    case .noStepFreeAccess:
      return Image("no.step.free.access")
    case .diverted:
      return Image("diverted")
    case .information:
      return Image("information")
    }
  }
}
7. TransportAPIService.swift
import Foundation
import Combine

enum HTTPError: LocalizedError {
  case statusCode
  case status
}

struct TransportAPILineStatus: Decodable {
  let friendlyName: String
  let status: String

  enum CodingKeys: String, CodingKey {
    case friendlyName = "friendly_name"
    case status = "status"
  }
}

struct TransportAPILinesResponse: Decodable {
  let bakerloo: TransportAPILineStatus
  let central: TransportAPILineStatus
  let circle: TransportAPILineStatus
  let district: TransportAPILineStatus
  let hammersmith: TransportAPILineStatus
  let jubilee: TransportAPILineStatus
  let metropolitan: TransportAPILineStatus
  let northern: TransportAPILineStatus
  let piccadilly: TransportAPILineStatus
  let victoria: TransportAPILineStatus
  let waterlooandcity: TransportAPILineStatus
  let dlr: TransportAPILineStatus
}

struct TransportAPIStatusResponse: Decodable {
  let requestTime: String
  let statusRefreshTime: String
  let lines: TransportAPILinesResponse

  enum CodingKeys: String, CodingKey {
    case requestTime = "request_time"
    case statusRefreshTime = "status_refresh_time"
    case lines = "lines"
  }
}

final class TransportAPIService {
  var cancellable: AnyCancellable?
  let dateFormatter = DateFormatter()

  let appId = Bundle.main.object(forInfoDictionaryKey: "TRANSPORT_API_SERVICE_APP_ID") as? String
  let appKey = Bundle.main.object(forInfoDictionaryKey: "TRANSPORT_API_SERVICE_APP_KEY") as? String

  init() {
    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss xxxx"
  }
}

// MARK: Data transformation
extension TransportAPIService {
  private func tflLineStatusFromAPIStatus(_ apiStatus: String) -> TFLLineStatus {
    let matchingStatus = TFLLineStatus.allCases.filter { $0.displayName() == apiStatus }

    if matchingStatus.isEmpty {
      #if DEBUG
      fatalError("Failed to find enum value for '\(apiStatus)', this is likely a programming error")
      #else
      return .unknown
      #endif
    } else {
      #if DEBUG
      if matchingStatus.count > 1 {
        fatalError("Found two enum status values for '\(apiStatus)'")
      }
      #endif
      return matchingStatus[0]
    }
  }

  func transportAPIStatusResponseToAllLinesStatus(_ apiResponse: TransportAPIStatusResponse) -> AllLinesStatus {
    let lastUpdated = apiResponse.statusRefreshTime
    let apiResponseLines = apiResponse.lines

    var lines: [LineStatus] = []
    let bakerlooLineStatus = LineStatus(
      line: bakerlooLine,
      status: tflLineStatusFromAPIStatus(apiResponseLines.bakerloo.status)
    )
    lines.append(bakerlooLineStatus)

    let centralLineStatus = LineStatus(
      line: centralLine,
      status: tflLineStatusFromAPIStatus(apiResponseLines.central.status)
    )
    lines.append(centralLineStatus)

    let circleLineStatus = LineStatus(
      line: circleLine,
      status: tflLineStatusFromAPIStatus(apiResponseLines.circle.status)
    )
    lines.append(circleLineStatus)

    let districtLineStatus = LineStatus(
      line: districtLine,
      status: tflLineStatusFromAPIStatus(apiResponseLines.district.status)
    )
    lines.append(districtLineStatus)

    let hammersmithLineStatus = LineStatus(
      line: hammersmithAndCityLine,
      status: tflLineStatusFromAPIStatus(apiResponseLines.hammersmith.status)
    )
    lines.append(hammersmithLineStatus)

    let jubileeLineStatus = LineStatus(
      line: jubileeLine,
      status: tflLineStatusFromAPIStatus(apiResponseLines.jubilee.status)
    )
    lines.append(jubileeLineStatus)

    let metropolitanLineStatus = LineStatus(
      line: metropolitanLine,
      status: tflLineStatusFromAPIStatus(apiResponseLines.metropolitan.status)
    )
    lines.append(metropolitanLineStatus)

    let northernLineStatus = LineStatus(
      line: northernLine,
      status: tflLineStatusFromAPIStatus(apiResponseLines.northern.status)
    )
    lines.append(northernLineStatus)

    let piccadillyLineStatus = LineStatus(
      line: piccadillyLine,
      status: tflLineStatusFromAPIStatus(apiResponseLines.piccadilly.status)
    )
    lines.append(piccadillyLineStatus)

    let victoriaLineStatus = LineStatus(
      line: victoriaLine,
      status: tflLineStatusFromAPIStatus(apiResponseLines.victoria.status)
    )
    lines.append(victoriaLineStatus)

    let waterlooAndCityLineStatus = LineStatus(
      line: waterlooAndCityLine,
      status: tflLineStatusFromAPIStatus(apiResponseLines.waterlooandcity.status)
    )
    lines.append(waterlooAndCityLineStatus)

    let dlrStatus = LineStatus(
      line: dlr,
      status: tflLineStatusFromAPIStatus(apiResponseLines.dlr.status)
    )
    lines.append(dlrStatus)

    let lastUpdatedDate = dateFormatter.date(from: lastUpdated)
    return AllLinesStatus(lastUpdated: lastUpdatedDate, linesStatus: lines)
  }
}

// MARK: TubeLinesStatusFetcher

extension TransportAPIService: TubeLinesStatusFetcher {
  func fetchStatus() -> Future<AllLinesStatus, Error> {
    guard
      let appId = appId,
      let appKey = appKey
    else {
      fatalError("Could no find a valid AppID or AppKey. Make sure you have setup your xcconfig file correctly")
    }

    var urlComponents = URLComponents()
    urlComponents.scheme = "https"
    urlComponents.host = "transportapi.com"
    urlComponents.path = "/v3/uk/tube/lines.json"
    urlComponents.queryItems = [
      URLQueryItem(name: "include_status", value: "true"),
      URLQueryItem(name: "app_id", value: appId),
      URLQueryItem(name: "app_key", value: appKey)
    ]

    guard let url = urlComponents.url else {
      fatalError("Failed to build URL, this is likely a programming error")
    }

    return Future { promise in
      self.cancellable = URLSession.shared.dataTaskPublisher(for: url)
        .tryMap { output in
          guard
            let response = output.response as? HTTPURLResponse,
            response.statusCode == 200
          else {
            throw HTTPError.statusCode
          }
          return output.data
        }
        .decode(type: TransportAPIStatusResponse.self, decoder: JSONDecoder())
        .sink(receiveCompletion: { completion in
          switch completion {
          case .finished:
            break
          case .failure(let error):
            DispatchQueue.main.async {
              promise(.failure(error))
            }
          }
        }, receiveValue: { [self] statusResponse in
          let allLinesStatus = transportAPIStatusResponseToAllLinesStatus(statusResponse)
          promise(.success(allLinesStatus))
        })
    }
  }
}
8. TubeLineStatusFetcher.swift
import SwiftUI
import Combine

struct LineStatus {
  let line: LineData
  let status: TFLLineStatus
}

struct AllLinesStatus {
  let lastUpdated: Date?
  let linesStatus: [LineStatus]
}

protocol TubeLinesStatusFetcher {
  func fetchStatus() -> Future<AllLinesStatus, Error>
}
9. ActivityIndicator.swift
import UIKit
import SwiftUI

struct ActivityIndicator: UIViewRepresentable {
  @Binding var isAnimating: Bool
  let style: UIActivityIndicatorView.Style

  func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
    UIActivityIndicatorView(style: style)
  }

  func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) {
    isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
  }
}
10. AttributedText.swift
import SwiftUI

struct AttributedText: View {
  @State private var size: CGSize = .zero

  let attributedString: NSAttributedString

  init(_ attributedString: NSAttributedString) {
    self.attributedString = attributedString
  }

  var body: some View {
    AttributedTextRepresentable(attributedString: attributedString, size: $size)
      .frame(width: size.width, height: size.height)
  }

  struct AttributedTextRepresentable: UIViewRepresentable {
    let attributedString: NSAttributedString
    @Binding var size: CGSize

    func makeUIView(context: Context) -> UILabel {
      let label = UILabel()

      label.lineBreakMode = .byClipping
      label.numberOfLines = 0

      return label
    }

    func updateUIView(_ uiView: UILabel, context: Context) {
      uiView.attributedText = attributedString

      DispatchQueue.main.async {
        size = uiView.sizeThatFits(uiView.superview?.bounds.size ?? .zero)
      }
    }
  }
}
11. LineStatusRow.swift
import SwiftUI

struct LineStatusRow: View {
  var lineDisplayName: String
  var status: TFLLineStatus
  var lineColor: Color

  var body: some View {
    HStack(alignment: .lastTextBaseline) {
      status.image()
        .font(.title)
        .padding(.trailing)
        .foregroundColor(lineColor.contrastingTextColor)
      VStack(alignment: .leading) {
        Text(lineDisplayName)
          .font(.caption)
          .fontWeight(.bold)
          .foregroundColor(lineColor.contrastingTextColor)
        Text(status.displayName())
          .font(.caption)
          .foregroundColor(lineColor.contrastingTextColor)
      }
      Spacer()
    }
    .padding()
    .frame(maxWidth: .infinity)
    .background(lineColor)
  }
}

struct LineStatusRow_Previews: PreviewProvider {
  static var previews: some View {
    LineStatusRow(
      lineDisplayName: "Bakerloo",
      status: TFLLineStatus.goodService,
      lineColor: Color(red: 137 / 255, green: 78 / 255, blue: 36 / 255)
    )
    LineStatusRow(
      lineDisplayName: "Bakerloo",
      status: TFLLineStatus.specialService,
      lineColor: Color(red: 137 / 255, green: 78 / 255, blue: 36 / 255)
    )
  }
}
12. TubeStatusView.swift
import SwiftUI

struct TubeStatusView: View {
  @ObservedObject private(set) var model: TubeStatusViewModel

  let titleText: NSAttributedString

  init(model: TubeStatusViewModel) {
    UINavigationBar.appearance().tintColor = UIColor(Color.tflBlue)

    self.model = model

    let imageAttachment = NSTextAttachment()
    imageAttachment.image = UIImage(systemName: "tram.fill")

    let title = NSMutableAttributedString(string: "Tube ")
    title.append(NSAttributedString(attachment: imageAttachment))
    title.append(NSAttributedString(string: " Status"))
    title.addAttribute(
      .font,
      value: UIFont.preferredFont(forTextStyle: .headline),
      range: NSRange(location: 0, length: title.length)
    )
    titleText = title
  }

  func loadData() {
    model.perform(action: .fetchCurrentStatus)
  }

  var body: some View {
    NavigationView {
      ZStack {
        Color(UIColor.systemGroupedBackground)
          .edgesIgnoringSafeArea(.all)
        Loadable(loadingState: model.tubeStatusState, hideContentWhenLoading: true) { tubeStatus in
          if tubeStatus.linesStatus.isEmpty {
            Text("Unexpected Error: No status to show")
          } else {
            ScrollView {
              LazyVStack(spacing: 0) {
                ForEach(tubeStatus.linesStatus) { lineStatus in
                  LineStatusRow(
                    lineDisplayName: lineStatus.displayName,
                    status: lineStatus.status,
                    lineColor: lineStatus.color
                  )
                }
                Text("\(tubeStatus.lastUpdated)")
                  .font(.footnote)
                  .padding()
              }
            }
          }
        }
      }
      .navigationBarTitleDisplayMode(.inline)
      .toolbar {
        ToolbarItem(placement: .principal) {
          AttributedText(titleText)
        }
      }
      .navigationBarItems(
        trailing:
          Button(action: {
            loadData()
          }, label: {
            Image(systemName: "arrow.clockwise.circle")
          })
      )
      .onAppear(perform: loadData)
    }
  }
}


struct TubeStatusView_Previews: PreviewProvider {
  static var previews: some View {
    let viewModel = TubeStatusViewModel(tubeLinesStatusFetcher: DebugDataService())
    TubeStatusView(model: viewModel)
  }
}

13. Loadable.swift
import SwiftUI

struct Loadable<T, Content: View>: View {
  let content: (T) -> Content
  let hideContentWhenLoading: Bool
  let loadingState: Loading<T>

  public init(loadingState: Loading<T>, @ViewBuilder content: @escaping (T) -> Content) {
    self.content = content
    self.hideContentWhenLoading = false
    self.loadingState = loadingState
  }

  public init(loadingState: Loading<T>, hideContentWhenLoading: Bool, @ViewBuilder content: @escaping (T) -> Content) {
    self.content = content
    self.hideContentWhenLoading = hideContentWhenLoading
    self.loadingState = loadingState
  }

  var body: some View {
    switch loadingState {
    case .loaded(let type), .updating(let type):
      return AnyView(content(type))
    case .loading(let type):
      return AnyView(
        ZStack {
          if !hideContentWhenLoading {
            content(type)
          }
          ActivityIndicator(isAnimating: .constant(true), style: .large)
            .opacity(0.9)
        }
      )
    case .errored:
      return AnyView(Text("Error loading view"))
    }
  }
}

struct Loadable_Previews: PreviewProvider {
  static var previews: some View {
    Loadable<String, Text>(loadingState: .loading("Loading...")) { string in
      Text(string)
    }
  }
}
14. Loading.swift
import Foundation

enum Loading<T> {
  case loading(T)
  case loaded(T)
  case updating(T)
  case errored(Error)
}
15. TubeLinesStatusFetcherFactory.swift
import Foundation

enum TubeLinesStatusFetcherFactory {
  static func new() -> TubeLinesStatusFetcher {
    #if DEBUG
    if ProcessInfo.processInfo.environment["USE_DEBUG_DATA"] == "true" {
      return DebugDataService()
    }
    #endif
    return TransportAPIService()
  }
}
16. TubeStatusViewModel.swift
import SwiftUI
import Combine

struct LineStatusModel: Identifiable {
  let id: String
  let displayName: String
  let status: TFLLineStatus
  let color: Color
}

struct TubeStatusModel {
  let lastUpdated: String
  let linesStatus: [LineStatusModel]
}

enum TubeStatusViewModelAction {
  case fetchCurrentStatus
}

final class TubeStatusViewModel: ObservableObject {
  let tubeLinesStatusFetcher: TubeLinesStatusFetcher
  let dateFormatter = DateFormatter()

  // MARK: - Publishers
  @Published var tubeStatusState: Loading<TubeStatusModel>

  var cancellable: AnyCancellable?

  init(tubeLinesStatusFetcher: TubeLinesStatusFetcher) {
    self.tubeLinesStatusFetcher = tubeLinesStatusFetcher
    dateFormatter.dateStyle = .full
    dateFormatter.timeStyle = .medium

    tubeStatusState = .loading(
      TubeStatusModel(lastUpdated: "", linesStatus: [])
    )
  }

  // MARK: Actions

  func perform(action: TubeStatusViewModelAction) {
    switch action {
    case .fetchCurrentStatus:
      fetchCurrentStatus()
    }
  }

  // MARK: Action handlers

  private func fetchCurrentStatus() {
    self.cancellable = tubeLinesStatusFetcher.fetchStatus()
      .sink(receiveCompletion: asyncCompletionErrorHandler) { allLinesStatus in
        DispatchQueue.main.async { [self] in
          let lastUpdatedDisplayValue: String
          if let updatedDate = allLinesStatus.lastUpdated {
            lastUpdatedDisplayValue = dateFormatter.string(from: updatedDate)
          } else {
            lastUpdatedDisplayValue = "Unknown"
          }

          tubeStatusState = .loaded(
            TubeStatusModel(
              lastUpdated: "Last Updated: \(lastUpdatedDisplayValue)",
              linesStatus: allLinesStatus.linesStatus.compactMap {
                LineStatusModel(
                  id: $0.line.name,
                  displayName: $0.line.name,
                  status: $0.status,
                  color: $0.line.color
                )
              })
          )
        }
      }
  }

  private func asyncCompletionErrorHandler(completion: Subscribers.Completion<Error>) {
    switch completion {
    case .failure(let error):
      tubeStatusState = .errored(error)
    case .finished: ()
    }
  }
}

後記

本篇主要講述了SF Symbols簡單使用介紹,感興趣的給個贊或者關注~~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章