SwiftUI框架詳細(xì)解析 (十一) —— 基于SwiftUI構(gòu)建各種自定義圖表(二)

版本記錄

版本號 時(shí)間
V1.0 2020.01.10 星期五

前言

今天翻閱蘋果的API文檔,發(fā)現(xiàn)多了一個(gè)框架SwiftUI,這里我們就一起來看一下這個(gè)框架。感興趣的看下面幾篇文章。
1. SwiftUI框架詳細(xì)解析 (一) —— 基本概覽(一)
2. SwiftUI框架詳細(xì)解析 (二) —— 基于SwiftUI的閃屏頁的創(chuàng)建(一)
3. SwiftUI框架詳細(xì)解析 (三) —— 基于SwiftUI的閃屏頁的創(chuàng)建(二)
4. SwiftUI框架詳細(xì)解析 (四) —— 使用SwiftUI進(jìn)行蘋果登錄(一)
5. SwiftUI框架詳細(xì)解析 (五) —— 使用SwiftUI進(jìn)行蘋果登錄(二)
6. SwiftUI框架詳細(xì)解析 (六) —— 基于SwiftUI的導(dǎo)航的實(shí)現(xiàn)(一)
7. SwiftUI框架詳細(xì)解析 (七) —— 基于SwiftUI的導(dǎo)航的實(shí)現(xiàn)(二)
8. SwiftUI框架詳細(xì)解析 (八) —— 基于SwiftUI的動畫的實(shí)現(xiàn)(一)
9. SwiftUI框架詳細(xì)解析 (九) —— 基于SwiftUI的動畫的實(shí)現(xiàn)(二)
10. SwiftUI框架詳細(xì)解析 (十) —— 基于SwiftUI構(gòu)建各種自定義圖表(一)

源碼

1. Swift

首先看下工程組織結(jié)構(gòu)

下面就是源碼了

1. TemperatureTab.swift
import SwiftUI

struct TemperatureTab: View {
  var station: WeatherStation
  
  var body: some View {
    VStack {
      Text("Temperatures for 2018")
      TemperatureChart(measurements: station.measurements)
    }.padding()
  }
}

struct TemperatureTab_Previews: PreviewProvider {
  static var previews: some View {
    TemperatureTab(station: WeatherInformation()!.stations[0])
  }
}
2. SnowfallTab.swift
import SwiftUI

struct SnowfallTab: View {
  var station: WeatherStation
  
  var body: some View {
    VStack {
      Text("Snowfall for 2018")
      SnowfallChart(snowfall: self.station.measurements.filter { $0.snowfall > 0.0 })
    }
  }
}

struct SnowfallTab_Previews: PreviewProvider {
  static var previews: some View {
    SnowfallTab(station: WeatherInformation()!.stations[2])
  }
}
3. PrecipitationTab.swift
import SwiftUI

struct PrecipitationTab: View {
  var station: WeatherStation
  
  func monthFromName(_ name: String) -> Int {
    let df = DateFormatter()
    df.dateFormat = "LLLL"
    if let date = df.date(from: name) {
      return Calendar.current.component(.month, from: date)
    }
    return 0
  }
    
  var body: some View {
    VStack {
      Text("Precipitation for 2018")
      PrecipitationChart(measurements: station.measurements)
    }
  }
}

struct PrecipitationTab_Previews: PreviewProvider {
  static var previews: some View {
    PrecipitationTab(station: WeatherInformation()!.stations[2])
  }
}
4. PrecipitationChart.swift
import SwiftUI

struct PrecipitationChart: View {
  var measurements: [DayInfo]
  
  func sumPrecipitation(_ month: Int) -> Double {
    self.measurements
      .filter {
        Calendar.current.component(.month, from: $0.date) == month + 1
    }
    .reduce(0) { $0 + $1.precipitation }
  }
  
  func monthAbbreviationFromInt(_ month: Int) -> String {
    let ma = Calendar.current.shortMonthSymbols
    return ma[month]
  }
  
  var body: some View {
    // 1
    HStack {
      // 2
      ForEach(0..<12) { month in
        // 3
        VStack {
          // 4
          Spacer()
          Text("\(self.sumPrecipitation(month).stringToOneDecimal)")
            .font(.footnote)
            .rotationEffect(.degrees(-90))
            .offset(y: self.sumPrecipitation(month) < 2.4 ? 0 : 35)
            .zIndex(1)
          // 5
          Rectangle()
            .fill(Color.green)
            .frame(width: 20, height: CGFloat(self.sumPrecipitation(month)) * 15.0)
          
          // 6
          Text("\(self.monthAbbreviationFromInt(month))")
            .font(.footnote)
            .frame(height: 20)
        }
      }
    }
  }
}

struct PrecipitationChart_Previews: PreviewProvider {
  static var previews: some View {
    PrecipitationChart(measurements: WeatherInformation()!.stations[2].measurements)
  }
}
5. SnowfallChart.swift
import SwiftUI

struct SnowfallChart: View {
  var snowfall: [DayInfo]
  
  var body: some View {
    // 1
    List(snowfall.filter { $0.snowfall > 0.0 }) { measurement in
      HStack {
        // 2
        Text("\(measurement.dateString)")
          .frame(width: 100, alignment: .trailing)
        // 3
        ZStack(alignment: .leading) {
          ForEach(0..<17) { mark in
            Rectangle()
              .fill(mark % 5 == 0 ? Color.black : Color.gray)
              .offset(x: CGFloat(mark) * 10.0)
              .frame(width: 1.0)
              .zIndex(1)
          }
          Rectangle()
            .fill(Color.blue)
            .frame(width: CGFloat(measurement.snowfall * 10.0), height: 5.0)
        }
        // 4
        Spacer()
        Text("\(measurement.snowfall.stringToOneDecimal)\"")
      }
    }
  }
}

struct SnowfallChart_Previews: PreviewProvider {
  static var previews: some View {
    SnowfallChart(snowfall: WeatherInformation()!.stations[2].measurements)
  }
}
6. TemperatureChart.swift
import SwiftUI

struct TemperatureChart: View {
  var measurements: [DayInfo]
  
  let tempGradient = Gradient(colors: [
    .purple,
    Color(red: 0, green: 0, blue: 139.0/255.0),
    .blue,
    Color(red: 30.0/255.0, green: 144.0/255.0, blue: 1.0),
    Color(red: 0, green: 191/255.0, blue: 1.0),
    Color(red: 135.0/255.0, green: 206.0/255.0, blue: 250.0/255.0),
    .green,
    .yellow,
    .orange,
    Color(red: 1.0, green: 140.0/255.0, blue: 0.0),
    .red,
    Color(red: 139.0/255.0, green: 0.0, blue: 0.0)
  ])
  
  func degreeHeight(_ height: CGFloat, range: Int) -> CGFloat {
    height / CGFloat(range)
  }
  
  func dayWidth(_ width: CGFloat, count: Int) -> CGFloat {
    width / CGFloat(count)
  }
  
  func dayOffset(_ date: Date, dWidth: CGFloat) -> CGFloat {
    CGFloat(Calendar.current.ordinality(of: .day, in: .year, for: date)!) * dWidth
  }
  
  func tempOffset(_ temperature: Double, degreeHeight: CGFloat) -> CGFloat {
    CGFloat(temperature + 10) * degreeHeight
  }
  
  func tempLabelOffset(_ line: Int, height: CGFloat) -> CGFloat {
    height - self.tempOffset(Double(line * 10),
                             degreeHeight: self.degreeHeight(height, range: 110))
  }
  
  func offsetFirstOfMonth(_ month: Int, width: CGFloat) -> CGFloat {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "M/d/yyyy"
    let foM = dateFormatter.date(from: "\(month)/1/2018")!
    let dayWidth = self.dayWidth(width, count: 365)
    return self.dayOffset(foM, dWidth: dayWidth)
  }
  
  func monthAbbreviationFromInt(_ month: Int) -> String {
    let ma = Calendar.current.shortMonthSymbols
    return ma[month - 1]
  }
  
  
  var body: some View {
    // 1
    GeometryReader { reader in
      ForEach(self.measurements) { measurement in
        // 2
        Path { p in
          // 3
          let dWidth = self.dayWidth(reader.size.width, count: 365)
          let dHeight = self.degreeHeight(reader.size.height, range: 110)
          // 4
          let dOffset = self.dayOffset(measurement.date, dWidth: dWidth)
          // 5
          let lowOffset = self.tempOffset(measurement.low, degreeHeight: dHeight)
          let highOffset = self.tempOffset(measurement.high, degreeHeight: dHeight)
          // 6
          p.move(to: .init(x: dOffset, y: reader.size.height - lowOffset))
          p.addLine(to: .init(x: dOffset, y: reader.size.height - highOffset))
          // 7
        }.stroke(LinearGradient(
          gradient: self.tempGradient,
          startPoint: .init(x: 0.0, y: 1.0),
          endPoint: .init(x: 0.0, y: 0.0)))
      }
      // 1
      ForEach(-1..<11) { line in
        // 2
        Group {
          Path { path in
            // 3
            let y = self.tempLabelOffset(line, height: reader.size.height)
            path.move(to: CGPoint(x: 0, y: y))
            path.addLine(to: CGPoint(x: reader.size.width, y: y))
            // 4
          }.stroke(line == 0 ? Color.black : Color.gray)
          // 5
          if line >= 0 {
            Text("\(line * 10)°")
              .offset(x: 10, y: self.tempLabelOffset(line, height: reader.size.height))
          }
        }
      }
      ForEach(1..<13) { month in
        Group {
          Path { path in
            let dOffset = self.offsetFirstOfMonth(month, width: reader.size.width)
            
            path.move(to: CGPoint(x: dOffset, y: reader.size.height))
            path.addLine(to: CGPoint(x: dOffset, y: 0))
          }.stroke(Color.gray)
          Text("\(self.monthAbbreviationFromInt(month))")
            .font(.subheadline)
            .offset(
              x: self.offsetFirstOfMonth(month, width: reader.size.width) +
                5 * self.dayWidth(reader.size.width, count: 365),
              y: reader.size.height - 25.0)
        }
      }
    }
  }
}



struct TemperatureChart_Previews: PreviewProvider {
  static var previews: some View {
    TemperatureChart(measurements: WeatherInformation()!.stations[2].measurements)
  }
}
7. WeatherInformation.swift
import Foundation

class WeatherInformation {
  var stations: [WeatherStation]
  
  init?() {
    // Init empty array
    stations = []

    // Converter for date string
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "M/d/yyyy"

    // Get CSV data
    guard let csvData = getCsvAsString() else { return nil }
    
    var currentStationId = ""
    var currentStation: WeatherStation?
    // Parse each line
    csvData.enumerateLines { (line, _) in
      let cols = line.components(separatedBy: ",")
      if currentStationId != cols[0] {
        if let newStation = currentStation {
          if newStation.name != "NAME" {
            self.stations.append(newStation)
          }
        }
        
        currentStationId = cols[0]
        let name = cols[1].replacingOccurrences(of: "\"", with: "").replacingOccurrences(of: ";", with: ",")
        let lat = Double(cols[2]) ?? 0
        let lng = Double(cols[3]) ?? 0
        let alt = Int((Double(cols[4]) ?? 0) * 3.28084) // m to ft.
        currentStation = WeatherStation(id: currentStationId, name: name, latitude: lat, longitude: lng, altitude: alt, measurements: [])
      }
      
      let date = dateFormatter.date(from: cols[5]) ?? dateFormatter.date(from: "1/1/2018")!
      let precip = Double(cols[6]) ?? 0
      let snow = Double(cols[7]) ?? 0
      let high = Double(cols[8]) ?? 0
      let low = Double(cols[9]) ?? 0
      let newData = DayInfo(date: date, precipitation: precip, snowfall: snow, high: high, low: low)
      
      currentStation?.measurements.append(newData)
    }
    // Add the last station read
    if let newStation = currentStation {
      self.stations.append(newStation)
    }
  }
  
  func getCsvAsString() -> String? {
    guard let fileURL = Bundle.main.url(forResource: "weather-data", withExtension: "csv") else { return nil }
    do {
      let csvData = try String(contentsOf: fileURL)
      return csvData
    } catch {
      return nil
    }
  }
}
8. WeatherStation.swift
import Foundation

struct WeatherStation: Identifiable {
  var id: String
  var name: String
  var latitude: Double
  var longitude: Double
  var altitude: Int
  var measurements: [DayInfo]
    
  func measurementsInMonth(_ month: Int) -> [DayInfo] {
    return measurements.filter {
      return Calendar.current.component(.month, from: $0.date) == month
    }
  }
  
  var lowTemperatureForYear: Double {
    measurements.min(by: { $0.low < $1.low })!.low
  }
  
  var highTemperatureForYear: Double {
    measurements.max(by: { $0.high < $1.high })!.high
  }
}
9. DayInfo.swift
import Foundation

struct DayInfo : Identifiable {
  var date: Date
  var precipitation: Double
  var snowfall: Double
  var high: Double
  var low: Double
  
  var id: Date {
    return date
  }
  
  var dateString: String {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "M/d/yyyy"
    return dateFormatter.string(from: date)
  }
}
10. DoubleExtension.swift
import Foundation

extension Double {
  var stringToOneDecimal: String {
    String(format: "%.1f", self)
  }

  var stringToTwoDecimals: String {
    String(format: "%.2f", self)
  }

  var stringRounded: String {
    String(format: "%.f", self.rounded())
  }
  
  var asLatitude: String {
    let deg = floor(self)
    let min = fabs(self.truncatingRemainder(dividingBy: 1) * 60.0).rounded()
    if self > 0 {
      return String(format: "%.f° %.f\" N", deg, min)
    } else if self < 0 {
      return String(format: "%.f° %.f\" S", -deg, min)
    }
    
    return "0°"
  }
  
  var asLongitude: String {
    let deg = floor(self)
    let min = fabs(self.truncatingRemainder(dividingBy: 1) * 60.0).rounded()
    if self > 0 {
      return String(format: "%.f° %.f\" E", deg, min)
    } else if self < 0 {
      return String(format: "%.f° %.f\" W", -deg, min)
    }
    
    return "0°"
  }
}
11. AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  // MARK: - UISceneSession Lifecycle
  
  func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    // Called when a new scene session is being created.
    // Use this method to select a configuration to create the new scene with.
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
  }
}
12. SceneDelegate.swift
import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?
  
  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
    
    // Use a UIHostingController as window root view controller
    if let windowScene = scene as? UIWindowScene {
      let window = UIWindow(windowScene: windowScene)
      window.rootViewController = UIHostingController(rootView: ContentView())
      self.window = window
      window.makeKeyAndVisible()
    }
  }
}
13. ContentView.swift
import SwiftUI

struct ContentView: View {
  let stations = WeatherInformation()
  
  var body: some View {
    NavigationView {
      VStack {
        List(stations!.stations) { station in
          NavigationLink(destination: StationInfo(station: station)) {
            Text("\(station.name)")
          }
        }
        Text("Source: https://www.ncdc.noaa.gov/cdo-web/datasets")
          .italic()
      }.navigationBarTitle(Text("Weather Stations"))
    }
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}
14. StationInfo.swift
import SwiftUI

struct StationInfo: View {
  var station: WeatherStation
  
    var body: some View {
      VStack {
        StationHeader(station: self.station)
          .padding()
        TabView {
          TemperatureTab(station: self.station)
            .tabItem({
              Image(systemName: "thermometer")
              Text("Temperatures")
            })
          SnowfallTab(station: self.station)
            .tabItem({
              Image(systemName: "snow")
              Text("Snowfall")
            })
            PrecipitationTab(station: self.station)
            .tabItem({
              Image(systemName: "cloud.rain")
              Text("Precipitation")
            })
        }
        }.navigationBarTitle(Text("\(station.name)"), displayMode: .inline)
    }
}

struct StationInfo_Previews: PreviewProvider {
    static var previews: some View {
      StationInfo(station: WeatherInformation()!.stations[0])
    }
}
15. StationHeader.swift
import SwiftUI

struct StationHeader: View {
  var station: WeatherStation
  
  var body: some View {
    HStack {
      VStack(alignment: .leading) {
        Text("Latitude: \(station.latitude.asLatitude)")
        Text("Longitude: \(station.longitude.asLongitude)")
        Text("Elevation: \(station.altitude) ft.")
      }
      Spacer()
      MapView(latitude: station.latitude, longitude: station.longitude)
        .frame(width: 200, height: 200)
    }
  }
}

struct StationHeader_Previews: PreviewProvider {
  static var previews: some View {
    StationHeader(station: WeatherInformation()!.stations[1])
  }
}
16. MapView.swift
import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
  var latitude: Double
  var longitude: Double
  
  func makeUIView(context: Context) -> MKMapView {
    MKMapView(frame: .zero)
  }
  
  func updateUIView(_ view: MKMapView, context: Context) {
    let coordinate = CLLocationCoordinate2D(
      latitude: self.latitude,
      longitude: self.longitude)
    let span = MKCoordinateSpan(latitudeDelta: 0.15, longitudeDelta: 0.15)
    let region = MKCoordinateRegion(center: coordinate, span: span)
    view.setRegion(region, animated: true)
    view.mapType = .hybrid
    view.isScrollEnabled = false
  }
}

struct MapView_Previews: PreviewProvider {
  static var previews: some View {
    MapView(latitude: 34.011286, longitude: -116.166868)
  }
}

后記

本篇主要講述了基于SwiftUI構(gòu)建各種自定義圖表,感興趣的給個(gè)贊或者關(guān)注~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容