投稿の経緯
この記事は、前回投稿したWidgetKitで天気予報アプリ作ってみたシリーズ~タイムライン作成編~の続編です。
今回はいよいよViewを実装していきます。
前回の記事を見てない人は先に↓こちら↓を確認してください。
環境
Swift 5.5
Xcode 13.2.1
サンプルプロジェクト
GitHubにPushしています。気になる方はご覧ください。
https://github.com/ken-sasaki-222/WeatherWidget
Viewで表示する内容
画像のように各ブロックごとに分割して開発していきます。周りの余白は16ではみ出す場合はclipped()
します。
Viewで表示する内容は以下の通り。
青ブロック
- 現在の気温
- 現在時刻
- 地点名
赤ブロック
- 気圧グラフ
緑ブロック
- 時刻テキスト
- 天気アイコン
- 気温
表示するデータは前回作成した「EntryModel」から取得して使います。
import WidgetKit
import SwiftUI
struct MediumWidgetEntryModel: TimelineEntry {
let date: Date
var hourlyWeathers: [Hourly]
var currentLocation: String?
var weatherIcons: [String]
var timePeriodTexts: [String]
var temperatureTexts: [String]
var hourlyPressures: [Double]
init(currentDate: Date, hourlyWeathers: [Hourly], currentLocation: String?) {
self.date = currentDate
self.currentLocation = currentLocation
self.hourlyWeathers = []
self.weatherIcons = []
self.timePeriodTexts = []
self.temperatureTexts = []
self.hourlyPressures = []
for index in 0..<24 {
self.hourlyWeathers.append(hourlyWeathers[index])
self.hourlyPressures.append(hourlyWeathers[index].pressure)
let timePeriodText = getTimePeriodText(hourlyWeather: hourlyWeathers[index])
self.timePeriodTexts.append(timePeriodText)
let weather = hourlyWeathers[index].weather[0]
if let weatherIconName = getWeatherIconName(weather: weather) {
self.weatherIcons.append(weatherIconName)
}
let temp = String(format: "%0.0f", hourlyWeathers[index].temp)
self.temperatureTexts.append(temp)
}
}
func getTimePeriodText(hourlyWeather: Hourly) -> String {
let date = Date(timeIntervalSince1970: hourlyWeather.dt)
let dateString = DateFormatHelper.shared.formatToHHmm(date: date)
var timePeriodText: String
if dateString == "00:00" {
timePeriodText = "0"
} else if dateString == "03:00" {
timePeriodText = "3"
} else if dateString == "06:00" {
timePeriodText = "6"
} else if dateString == "09:00" {
timePeriodText = "9"
} else if dateString == "12:00" {
timePeriodText = "12"
} else if dateString == "15:00" {
timePeriodText = "15"
} else if dateString == "18:00" {
timePeriodText = "18"
} else if dateString == "21:00" {
timePeriodText = "21"
} else {
timePeriodText = "・"
}
return timePeriodText
}
func getWeatherIconName(weather: Weather) -> String? {
var wetherName: String
switch weather.main {
case "Clear":
wetherName = WeatherTypeTranslator.translate(type: .clear)
case "Clouds":
wetherName = WeatherTypeTranslator.translate(type: .clouds)
case "Rain":
wetherName = WeatherTypeTranslator.translate(type: .rain)
case "Snow":
wetherName = WeatherTypeTranslator.translate(type: .snow)
default:
return nil
}
return wetherName
}
}
このタイミングで「EntryModel」の値をインスタンス化しておきましょう。
struct MediumWidgetView: View {
@Environment(\.colorScheme) var colorScheme
var entry: MediumWidgetProvider.Entry
var body: some View {
let currentDate = DateFormatHelper.shared.formatToHH(date: entry.date)
let hourlyWeathers = entry.hourlyWeathers
let timePeriodTexts = entry.timePeriodTexts
let weatherIcons = entry.weatherIcons
let temperatureTexts = entry.temperatureTexts
let hourlyPressures = entry.hourlyPressures
GeometryReader { geometry in
let geometryWidth = geometry.size.width
let geometryHeight = geometry.size.height
let widthPerHour = (geometryWidth - 32) / 24 // 1時間あたりのwidth
VStack {
Spacer().frame(height: 16)
VStack(spacing: 0) {
// 現在気温、現在時刻、地点名(青ブロック)
HStack {
Spacer()
HStack(spacing: 0) {
}
.frame(width: geometryWidth - 32, height: geometryHeight / 6)
.background(Color.blue)
Spacer()
}
// グラフ(赤ブロック)
HStack {
Spacer()
HStack {
}
.frame(width: geometryWidth - 32, height: geometryHeight / 3)
.background(Color.red)
Spacer()
}
// 時刻、天気アイコン、気温(緑ブロック)
HStack {
Spacer()
HStack(alignment: .top, spacing: 0) {
}
.frame(width: geometryWidth - 32, height: geometryHeight / 3)
.background(Color.green)
Spacer()
}
}
}
}
.background(ColorManager.background)
}
}
struct MediumWidgetView_Previews: PreviewProvider {
static var previews: some View {
let entry = MediumWidgetEntryModel(
currentDate: Date(timeIntervalSince1970: 1644048000),
hourlyWeathers: MockHourly.data,
currentLocation: "世田谷区"
)
Group {
MediumWidgetView(entry: entry)
.previewContext(WidgetPreviewContext(family: .systemMedium))
.environment(\.colorScheme, .light)
MediumWidgetView(entry: entry)
.previewContext(WidgetPreviewContext(family: .systemMedium))
.environment(\.colorScheme, .dark)
}
}
}
-
currentDate
-> 現在時刻 -
hourlyWeathers
-> 24時間分の天気データ -
timePeriodTexts
-> 時刻テキスト -
weatherIcons
-> 天気アイコン -
temperatureTexts
-> 気温 -
hourlyPressures
-> 気圧
Viewのwidth
、height
はGeometryReader
で取得しており、こちらもインスタンス化してViewの実装を進めます。
DateFormatHelper
の中身はこちら。
import Foundation
final class DateFormatHelper {
static let shared = DateFormatHelper()
private let hourAndMinutesFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "Asia/Tokyo")
formatter.dateFormat = "HH:mm"
return formatter
}()
private let hourFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "Asia/Tokyo")
formatter.dateFormat = "HH"
return formatter
}()
func formatToHHmm(date: Date) -> String {
return hourAndMinutesFormatter.string(from: date)
}
func formatToHH(date: Date) -> String {
return hourFormatter.string(from: date)
}
}
View完成イメージ
現在の気温、現在時、地点名の実装(青ブロック)
struct MediumWidgetView: View {
@Environment(\.colorScheme) var colorScheme
var entry: MediumWidgetProvider.Entry
var body: some View {
let currentDate = DateFormatHelper.shared.formatToHH(date: entry.date)
let hourlyWeathers = entry.hourlyWeathers
let timePeriodTexts = entry.timePeriodTexts
let weatherIcons = entry.weatherIcons
let temperatureTexts = entry.temperatureTexts
let hourlyPressures = entry.hourlyPressures
GeometryReader { geometry in
let geometryWidth = geometry.size.width
let geometryHeight = geometry.size.height
let widthPerHour = (geometryWidth - 32) / 24 // 1時間あたりのwidth
VStack {
Spacer().frame(height: 16)
VStack(spacing: 0) {
// 現在気温、現在時刻、地点名(青ブロック)
HStack {
Spacer()
HStack(spacing: 0) {
HStack(spacing: 0) {
Text(String(format: "%0.0f", hourlyWeathers[0].temp))
.foregroundColor(ColorManager.font)
.font(.system(size: 30, weight: .semibold))
.offset(x: 5)
.fixedSize(horizontal: true, vertical: true)
Text("℃" + " \(currentDate):00" + "現在")
.foregroundColor(ColorManager.font)
.font(.system(size: 14, weight: .medium))
.offset(x: 5)
.fixedSize(horizontal: true, vertical: true)
.frame(height: geometryHeight / 6, alignment: .bottom)
}
.frame(width: (geometryWidth - 32) / 2, height: geometryHeight / 6, alignment: .leading)
if let location = entry.currentLocation {
Text(location)
.foregroundColor(ColorManager.font)
.font(.system(size: 14, weight: .medium))
.offset(x: -5)
.frame(width: (geometryWidth - 32) / 2, height: geometryHeight / 6, alignment: .bottomTrailing)
}
}
.frame(width: geometryWidth - 32, height: geometryHeight / 6)
Spacer()
}
// グラフ(赤ブロック)
HStack {
Spacer()
HStack {
}
.frame(width: geometryWidth - 32, height: geometryHeight / 3)
.background(Color.red)
Spacer()
}
// 時刻、天気アイコン、気温(緑ブロック)
HStack {
Spacer()
HStack(alignment: .top, spacing: 0) {
}
.frame(width: geometryWidth - 32, height: geometryHeight / 3)
.background(Color.green)
Spacer()
}
}
}
}
.background(ColorManager.background)
}
}
struct MediumWidgetView_Previews: PreviewProvider {
static var previews: some View {
let entry = MediumWidgetEntryModel(
currentDate: Date(timeIntervalSince1970: 1644048000),
hourlyWeathers: MockHourly.data,
currentLocation: "世田谷区"
)
Group {
MediumWidgetView(entry: entry)
.previewContext(WidgetPreviewContext(family: .systemMedium))
.environment(\.colorScheme, .light)
MediumWidgetView(entry: entry)
.previewContext(WidgetPreviewContext(family: .systemMedium))
.environment(\.colorScheme, .dark)
}
}
}
色はColorManager
で管理して、ダークモードにも対応しています。
struct ColorManager {
static let font = Color("font_color")
static let background = Color("background_color")
static let graph = Color("graph_color")
static let graphBackground = Color("graphbackground_color")
}
これで、現在の気温、現在時、地点名の実装ができました。特に難しいことはしていないと思うので説明はしません(笑)
現在の気温、現在時、地点名を実装後のプレビュー
気圧グラフの実装(赤ブロック)
import WidgetKit
import SwiftUI
struct PressureGraphPoint: Identifiable {
var id = UUID()
var points: [CGPoint] = []
}
struct MediumWidgetView: View {
@Environment(\.colorScheme) var colorScheme
var entry: MediumWidgetProvider.Entry
var body: some View {
let currentDate = DateFormatHelper.shared.formatToHH(date: entry.date)
let hourlyWeathers = entry.hourlyWeathers
let timePeriodTexts = entry.timePeriodTexts
let weatherIcons = entry.weatherIcons
let temperatureTexts = entry.temperatureTexts
let hourlyPressures = entry.hourlyPressures
GeometryReader { geometry in
let geometryWidth = geometry.size.width
let geometryHeight = geometry.size.height
let widthPerHour = (geometryWidth - 32) / 24 // 1時間あたりのwidth
VStack {
Spacer().frame(height: 16)
VStack(spacing: 0) {
// 現在気温、現在時刻、地点名(青ブロック)
HStack {
Spacer()
HStack(spacing: 0) {
// 省略
}
.frame(width: geometryWidth - 32, height: geometryHeight / 6)
Spacer()
}
// グラフ(赤ブロック)
HStack {
Spacer()
HStack {
GeometryReader { graphGeometry in
let graphGeometryWidth = graphGeometry.size.width
let graphGeometryHeight = graphGeometry.size.height
let graphBackLineStartPoint = (widthPerHour * 0.5)
let graphBackLineEndPoint = graphGeometryWidth - graphBackLineStartPoint
let pressureGraphPoints = getPressureGraphPoints(hourlyPressures: hourlyPressures, width: graphGeometryWidth, height: graphGeometryHeight)
ZStack {
// 背景ライン
Path { path in
path.move(to: CGPoint(x: graphBackLineStartPoint + widthPerHour, y:3))
path.addLine(to: CGPoint(x: graphBackLineEndPoint, y: 3))
path.move(to: CGPoint(x: graphBackLineStartPoint, y: graphGeometryHeight / 2))
path.addLine(to: CGPoint(x: graphBackLineEndPoint, y: graphGeometryHeight / 2))
path.move(to: CGPoint(x: graphBackLineStartPoint + widthPerHour, y: graphGeometryHeight - 3))
path.addLine(to: CGPoint(x: graphBackLineEndPoint, y: graphGeometryHeight - 3))
}
.stroke(ColorManager.graphBackground, lineWidth: 1)
// 気圧グラフ
ForEach(pressureGraphPoints) { pressureGraphPoint in
Path { path in
path.move(to: pressureGraphPoint.points[0])
for index in 1..<pressureGraphPoint.points.count {
path.addLine(to: pressureGraphPoint.points[index])
}
}
.stroke(ColorManager.graph, lineWidth: 3)
.offset(x: widthPerHour * 0.5)
.clipped()
}
Text(String(format: "%0.0f", hourlyPressures[0] + 15))
.font(.system(size: 6, weight: .regular))
.foregroundColor(ColorManager.font)
.frame(width: graphBackLineEndPoint, height: graphGeometryHeight, alignment: .topLeading)
Text(String(format: "%0.0f", hourlyPressures[0]))
.font(.system(size: 6, weight: .regular))
.foregroundColor(ColorManager.font)
.offset(y: 5)
.frame(width: graphBackLineEndPoint, height: graphGeometryHeight, alignment: .leading)
Text(String(format: "%0.0f", hourlyPressures[0] - 15))
.font(.system(size: 6, weight: .regular))
.foregroundColor(ColorManager.font)
.frame(width: graphBackLineEndPoint, height: graphGeometryHeight, alignment: .bottomLeading)
Text("hpa")
.font(.system(size: 8, weight: .regular))
.foregroundColor(ColorManager.font)
.offset(x: -4, y: -4)
.frame(width: graphBackLineEndPoint, height: graphGeometryHeight, alignment: .bottomTrailing)
}
}
}
.frame(width: geometryWidth - 32, height: geometryHeight / 3)
Spacer()
}
// 時刻、天気アイコン、気温(緑ブロック)
HStack {
Spacer()
HStack(alignment: .top, spacing: 0) {
}
.frame(width: geometryWidth - 32, height: geometryHeight / 3)
.background(Color.green)
Spacer()
}
}
}
}
.background(ColorManager.background)
}
private func getPressureGraphPoints(hourlyPressures: [Double], width: CGFloat, height: CGFloat) -> [PressureGraphPoint] {
let currentPressure = hourlyPressures[0]
var pressureGraphPoints: [PressureGraphPoint] = []
var tempPressurePoint = PressureGraphPoint()
hourlyPressures.enumerated().forEach { index, hourlyPressure in
let pressureGraphPointWidth = (width / 24) * CGFloat(index) // 各時刻のグラフ描画のx軸
let heightPerHpa = height / 30
let maxHpa = currentPressure + 15
let diffPressure = maxHpa - hourlyPressure
let pressureGraphPointHeight = diffPressure * heightPerHpa// 各時刻のグラフ描画のy軸
let points = CGPoint(x: pressureGraphPointWidth, y: pressureGraphPointHeight)
tempPressurePoint.points.append(points)
}
pressureGraphPoints.append(tempPressurePoint)
return pressureGraphPoints
}
}
struct MediumWidgetView_Previews: PreviewProvider {
static var previews: some View {
let entry = MediumWidgetEntryModel(
currentDate: Date(timeIntervalSince1970: 1644048000),
hourlyWeathers: MockHourly.data,
currentLocation: "世田谷区"
)
Group {
MediumWidgetView(entry: entry)
.previewContext(WidgetPreviewContext(family: .systemMedium))
.environment(\.colorScheme, .light)
MediumWidgetView(entry: entry)
.previewContext(WidgetPreviewContext(family: .systemMedium))
.environment(\.colorScheme, .dark)
}
}
}
気圧グラフの背景ラインと気圧グラフはPath
を用いて描画します。Pathで描画するコードをあまり複雑にしたくなかったので、getPressureGraphPoints
に、新たに追加したGeometryReader
で取得したwidthとheightを渡し、PressureGraphPoint型
のCGPintの配列を返しています。
getPressureGraphPoints
の以下の部分で気圧グラフ24時間分の描画ポイントを求めています。
let pressureGraphPointWidth = (width / 24) * CGFloat(index) // 各時刻のグラフ描画のx軸
let heightPerHpa = height / 30
let maxHpa = currentPressure + 15
let diffPressure = maxHpa - hourlyPressure
let pressureGraphPointHeight = diffPressure * heightPerHpa// 各時刻のグラフ描画のy軸
気圧グラフ描画のx軸は、描画範囲を24分割してindexと積すれば求められます。
y軸は少し複雑ですが、グラフ描画範囲を30で割った高さを1hpaあたりの高さとし、現在時刻の気圧に15を足した値を描画範囲の上限値としています。その上限値から各時間の気圧との差分を求めて、1hpaあたりの高さと積すれば各時間の気圧グラフ描画のy軸が求められます。今回のパターンだと気圧グラフ描画範囲の上限下限は現在時刻の気圧から15hpaということになります。
この計算が気圧グラフ描画の肝になると思いますが、結構苦労しました、、、
うまく伝われば幸いです。
ちなみに今回は「Open Weather API」のレスポンスで気圧の欠測値が確認できなかったので、欠測値を意識したコードは書いていません。(探せばありそう)
気圧グラフ実装後のプレビュー
気圧の値はモックで設定しています。実際にビルドすると小数点が影響してもう少し滑らかな描画になります。
時刻テキスト、天気アイコン、気温の実装(赤ブロック)
import WidgetKit
import SwiftUI
struct PressureGraphPoint: Identifiable {
var id = UUID()
var points: [CGPoint] = []
}
struct MediumWidgetView: View {
@Environment(\.colorScheme) var colorScheme
var entry: MediumWidgetProvider.Entry
var body: some View {
let currentDate = DateFormatHelper.shared.formatToHH(date: entry.date)
let hourlyWeathers = entry.hourlyWeathers
let timePeriodTexts = entry.timePeriodTexts
let weatherIcons = entry.weatherIcons
let temperatureTexts = entry.temperatureTexts
let hourlyPressures = entry.hourlyPressures
GeometryReader { geometry in
let geometryWidth = geometry.size.width
let geometryHeight = geometry.size.height
let widthPerHour = (geometryWidth - 32) / 24 // 1時間あたりのwidth
VStack {
Spacer().frame(height: 16)
VStack(spacing: 0) {
// 現在気温、現在時刻、地点名(青ブロック)
HStack {
Spacer()
HStack(spacing: 0) {
// 省略
}
.frame(width: geometryWidth - 32, height: geometryHeight / 6)
Spacer()
}
// グラフ(赤ブロック)
HStack {
Spacer()
HStack {
// 省略
}
.frame(width: geometryWidth - 32, height: geometryHeight / 3)
Spacer()
}
// 時刻、天気アイコン、気温(緑ブロック)
HStack {
Spacer()
HStack(alignment: .top, spacing: 0) {
ForEach(0..<hourlyWeathers.count) { index in
VStack(alignment: .center, spacing: 0) {
Text(timePeriodTexts[index])
.foregroundColor(ColorManager.font)
.font(.system(size: 14, weight: .medium))
.fixedSize(horizontal: true, vertical: true)
.frame(width: widthPerHour, alignment: .center)
if isMultipleOfThree(hourlyWeather: hourlyWeathers[index]) {
Image(weatherIcons[index])
.resizable()
.scaledToFill()
.frame(width: widthPerHour, height: 27)
.fixedSize(horizontal: true, vertical: true)
Text("\(temperatureTexts[index])℃")
.foregroundColor(ColorManager.font)
.font(.system(size: 12, weight: .medium))
.fixedSize(horizontal: true, vertical: true)
.frame(width: widthPerHour, alignment: .center)
}
}
}
}
.frame(width: geometryWidth - 32, height: geometryHeight / 3)
Spacer()
}
}
}
}
.background(ColorManager.background)
}
private func isMultipleOfThree(hourlyWeather: Hourly) -> Bool {
let hourDate = hourlyWeather.dt
let date = Date(timeIntervalSince1970: hourDate)
guard let dateInt = Int(DateFormatHelper.shared.formatToHH(date: date)) else {
return false
}
if dateInt % 3 == 0 {
return true
} else {
return false
}
}
// 省略
}
struct MediumWidgetView_Previews: PreviewProvider {
static var previews: some View {
let entry = MediumWidgetEntryModel(
currentDate: Date(timeIntervalSince1970: 1644048000),
hourlyWeathers: MockHourly.data,
currentLocation: "世田谷区"
)
Group {
MediumWidgetView(entry: entry)
.previewContext(WidgetPreviewContext(family: .systemMedium))
.environment(\.colorScheme, .light)
MediumWidgetView(entry: entry)
.previewContext(WidgetPreviewContext(family: .systemMedium))
.environment(\.colorScheme, .dark)
}
}
}
24時間分の時刻テキストと、天気アイコンと、気温を表示しています。新たに追加したisMultipleOfThree
で3の倍数の時間のみ天気アイコンと気温を表示するように判断しています。
時刻テキスト、天気アイコン、気温の実装後のプレビュー
コード全体
import WidgetKit
import SwiftUI
struct PressureGraphPoint: Identifiable {
var id = UUID()
var points: [CGPoint] = []
}
struct MediumWidgetView: View {
@Environment(\.colorScheme) var colorScheme
var entry: MediumWidgetProvider.Entry
var body: some View {
let currentDate = DateFormatHelper.shared.formatToHH(date: entry.date)
let hourlyWeathers = entry.hourlyWeathers
let timePeriodTexts = entry.timePeriodTexts
let weatherIcons = entry.weatherIcons
let temperatureTexts = entry.temperatureTexts
let hourlyPressures = entry.hourlyPressures
GeometryReader { geometry in
let geometryWidth = geometry.size.width
let geometryHeight = geometry.size.height
let widthPerHour = (geometryWidth - 32) / 24 // 1時間あたりのwidth
VStack {
Spacer().frame(height: 16)
VStack(spacing: 0) {
// 現在気温、現在時刻、地点名
HStack {
Spacer()
HStack(spacing: 0) {
HStack(spacing: 0) {
Text(String(format: "%0.0f", hourlyWeathers[0].temp))
.foregroundColor(ColorManager.font)
.font(.system(size: 30, weight: .semibold))
.offset(x: 5)
.fixedSize(horizontal: true, vertical: true)
Text("℃" + " \(currentDate):00" + "現在")
.foregroundColor(ColorManager.font)
.font(.system(size: 14, weight: .medium))
.offset(x: 5)
.fixedSize(horizontal: true, vertical: true)
.frame(height: geometryHeight / 6, alignment: .bottom)
}
.frame(width: (geometryWidth - 32) / 2, height: geometryHeight / 6, alignment: .leading)
if let location = entry.currentLocation {
Text(location)
.foregroundColor(ColorManager.font)
.font(.system(size: 14, weight: .medium))
.offset(x: -5)
.frame(width: (geometryWidth - 32) / 2, height: geometryHeight / 6, alignment: .bottomTrailing)
}
}
.frame(width: geometryWidth - 32, height: geometryHeight / 6)
Spacer()
}
// グラフ
HStack {
Spacer()
HStack {
GeometryReader { graphGeometry in
let graphGeometryWidth = graphGeometry.size.width
let graphGeometryHeight = graphGeometry.size.height
let graphBackLineStartPoint = (widthPerHour * 0.5)
let graphBackLineEndPoint = graphGeometryWidth - graphBackLineStartPoint
let pressureGraphPoints = getPressureGraphPoints(hourlyPressures: hourlyPressures, width: graphGeometryWidth, height: graphGeometryHeight)
ZStack {
// 背景ライン
Path { path in
path.move(to: CGPoint(x: graphBackLineStartPoint + widthPerHour, y:3))
path.addLine(to: CGPoint(x: graphBackLineEndPoint, y: 3))
path.move(to: CGPoint(x: graphBackLineStartPoint, y: graphGeometryHeight / 2))
path.addLine(to: CGPoint(x: graphBackLineEndPoint, y: graphGeometryHeight / 2))
path.move(to: CGPoint(x: graphBackLineStartPoint + widthPerHour, y: graphGeometryHeight - 3))
path.addLine(to: CGPoint(x: graphBackLineEndPoint, y: graphGeometryHeight - 3))
}
.stroke(ColorManager.graphBackground, lineWidth: 1)
// 気圧グラフ
ForEach(pressureGraphPoints) { pressureGraphPoint in
Path { path in
path.move(to: pressureGraphPoint.points[0])
for index in 1..<pressureGraphPoint.points.count {
path.addLine(to: pressureGraphPoint.points[index])
}
}
.stroke(ColorManager.graph, lineWidth: 3)
.offset(x: widthPerHour * 0.5) // timePeriodTextsのx軸と合わせて描画
.clipped()
}
Text(String(format: "%0.0f", hourlyPressures[0] + 15))
.font(.system(size: 6, weight: .regular))
.foregroundColor(ColorManager.font)
.frame(width: graphBackLineEndPoint, height: graphGeometryHeight, alignment: .topLeading)
Text(String(format: "%0.0f", hourlyPressures[0]))
.font(.system(size: 6, weight: .regular))
.foregroundColor(ColorManager.font)
.offset(y: 5)
.frame(width: graphBackLineEndPoint, height: graphGeometryHeight, alignment: .leading)
Text(String(format: "%0.0f", hourlyPressures[0] - 15))
.font(.system(size: 6, weight: .regular))
.foregroundColor(ColorManager.font)
.frame(width: graphBackLineEndPoint, height: graphGeometryHeight, alignment: .bottomLeading)
Text("hpa")
.font(.system(size: 8, weight: .regular))
.foregroundColor(ColorManager.font)
.offset(x: -4, y: -4)
.frame(width: graphBackLineEndPoint, height: graphGeometryHeight, alignment: .bottomTrailing)
}
}
}
.frame(width: geometryWidth - 32, height: geometryHeight / 3)
Spacer()
}
// 時刻、天気アイコン、気温
HStack {
Spacer()
HStack(alignment: .top, spacing: 0) {
ForEach(0..<hourlyWeathers.count) { index in
VStack(alignment: .center, spacing: 0) {
Text(timePeriodTexts[index])
.foregroundColor(ColorManager.font)
.font(.system(size: 14, weight: .medium))
.fixedSize(horizontal: true, vertical: true)
.frame(width: widthPerHour, alignment: .center)
if isMultipleOfThree(hourlyWeather: hourlyWeathers[index]) {
Image(weatherIcons[index])
.resizable()
.scaledToFill()
.frame(width: widthPerHour, height: 27)
.fixedSize(horizontal: true, vertical: true)
Text("\(temperatureTexts[index])℃")
.foregroundColor(ColorManager.font)
.font(.system(size: 12, weight: .medium))
.fixedSize(horizontal: true, vertical: true)
.frame(width: widthPerHour, alignment: .center)
}
}
}
}
.frame(width: geometryWidth - 32, height: geometryHeight / 3)
Spacer()
}
}
}
}
.background(ColorManager.background)
}
private func isMultipleOfThree(hourlyWeather: Hourly) -> Bool {
let hourDate = hourlyWeather.dt
let date = Date(timeIntervalSince1970: hourDate)
guard let dateInt = Int(DateFormatHelper.shared.formatToHH(date: date)) else {
return false
}
if dateInt % 3 == 0 {
return true
} else {
return false
}
}
private func getPressureGraphPoints(hourlyPressures: [Double], width: CGFloat, height: CGFloat) -> [PressureGraphPoint] {
let currentPressure = hourlyPressures[0]
var pressureGraphPoints: [PressureGraphPoint] = []
var tempPressurePoint = PressureGraphPoint()
hourlyPressures.enumerated().forEach { index, hourlyPressure in
let pressureGraphPointWidth = (width / 24) * CGFloat(index)
let heightPerHpa = height / 30
let maxHpa = currentPressure + 15
let diffPressure = maxHpa - hourlyPressure
let pressureGraphPointHeight = diffPressure * heightPerHpa
let points = CGPoint(x: pressureGraphPointWidth, y: pressureGraphPointHeight)
tempPressurePoint.points.append(points)
}
pressureGraphPoints.append(tempPressurePoint)
return pressureGraphPoints
}
}
struct MediumWidgetView_Previews: PreviewProvider {
static var previews: some View {
let entry = MediumWidgetEntryModel(
currentDate: Date(timeIntervalSince1970: 1644048000),
hourlyWeathers: MockHourly.data,
currentLocation: "世田谷区"
)
Group {
MediumWidgetView(entry: entry)
.previewContext(WidgetPreviewContext(family: .systemMedium))
.environment(\.colorScheme, .light)
MediumWidgetView(entry: entry)
.previewContext(WidgetPreviewContext(family: .systemMedium))
.environment(\.colorScheme, .dark)
}
}
}
おわりに
今回でWidgetKitで天気予報アプリ作ってみたシリーズの完結です。
Viewの記事を書くのはなかなか難しかったですが、誰かの役に立てば幸いです。
一通り「Widget Extension」でアプリを開発してみた感想としては、「Provider」と「EntryModel」でタイムラインを作る箇所の理解に苦戦しました。ただ、タイムライン作成の流れと、正確なModelを作ることさえできれば残りはModel(Entry)からデータを取得してViewを書くだけなので、慣れてしまえばそこまで複雑には感じなくなりました。
ご覧いただきありがとうございました。
こうしたほうがいいや、ここはちょっと違うなど気になる箇所があった場合、ご教示いただけると幸いです。質問も受け付けています。
お知らせ
現在副業でiOSアプリ開発を募集しています。
Twitter DMでご依頼お待ちしております🙂
↓活動リンクはこちら↓
https://linktr.ee/sasaki.ken