SF Symbols详细介绍(三) —— 简单使用介绍(二)
2021-05-23 本文已影响0人
刀客传奇
版本记录
版本号 | 时间 |
---|---|
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
简单使用介绍,感兴趣的给个赞或者关注~~~
