Commit 3abd9e14 by Dmitriy Stepanets

Working on graph line layout

parent 3cc5019d
...@@ -90,7 +90,7 @@ ...@@ -90,7 +90,7 @@
</EnvironmentVariable> </EnvironmentVariable>
<EnvironmentVariable <EnvironmentVariable
key = "_XCWidgetFamily" key = "_XCWidgetFamily"
value = "small" value = "medium"
isEnabled = "NO"> isEnabled = "NO">
</EnvironmentVariable> </EnvironmentVariable>
</EnvironmentVariables> </EnvironmentVariables>
......
...@@ -27,8 +27,10 @@ ...@@ -27,8 +27,10 @@
CD7D3180268F04DD000D01FA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD7D316C268EF219000D01FA /* Assets.xcassets */; }; CD7D3180268F04DD000D01FA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD7D316C268EF219000D01FA /* Assets.xcassets */; };
CD7D3182268F0C60000D01FA /* OneWeatherUI+Global.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7D3181268F0C60000D01FA /* OneWeatherUI+Global.swift */; }; CD7D3182268F0C60000D01FA /* OneWeatherUI+Global.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7D3181268F0C60000D01FA /* OneWeatherUI+Global.swift */; };
CD7D3187268F1F2E000D01FA /* UIImage+Resize.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7D3186268F1F2E000D01FA /* UIImage+Resize.swift */; }; CD7D3187268F1F2E000D01FA /* UIImage+Resize.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD7D3186268F1F2E000D01FA /* UIImage+Resize.swift */; };
CDC2E75626970D4A00B410D4 /* MediumWidgetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC2E75526970D4A00B410D4 /* MediumWidgetViewModel.swift */; };
CDC3F85A26946D0700AAE3BF /* HourlyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC3F85926946D0700AAE3BF /* HourlyView.swift */; }; CDC3F85A26946D0700AAE3BF /* HourlyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC3F85926946D0700AAE3BF /* HourlyView.swift */; };
CDC3F85C269471C900AAE3BF /* SF-Pro-Display-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = CDC3F85B269471C900AAE3BF /* SF-Pro-Display-Bold.otf */; }; CDC3F85C269471C900AAE3BF /* SF-Pro-Display-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = CDC3F85B269471C900AAE3BF /* SF-Pro-Display-Bold.otf */; };
CDE43FD62695C65700DBE79E /* HourlyTemps.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE43FD52695C65700DBE79E /* HourlyTemps.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
...@@ -69,8 +71,10 @@ ...@@ -69,8 +71,10 @@
CD7D317E268F00EF000D01FA /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; CD7D317E268F00EF000D01FA /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
CD7D3181268F0C60000D01FA /* OneWeatherUI+Global.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OneWeatherUI+Global.swift"; sourceTree = "<group>"; }; CD7D3181268F0C60000D01FA /* OneWeatherUI+Global.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OneWeatherUI+Global.swift"; sourceTree = "<group>"; };
CD7D3186268F1F2E000D01FA /* UIImage+Resize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resize.swift"; sourceTree = "<group>"; }; CD7D3186268F1F2E000D01FA /* UIImage+Resize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resize.swift"; sourceTree = "<group>"; };
CDC2E75526970D4A00B410D4 /* MediumWidgetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediumWidgetViewModel.swift; sourceTree = "<group>"; };
CDC3F85926946D0700AAE3BF /* HourlyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HourlyView.swift; sourceTree = "<group>"; }; CDC3F85926946D0700AAE3BF /* HourlyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HourlyView.swift; sourceTree = "<group>"; };
CDC3F85B269471C900AAE3BF /* SF-Pro-Display-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Bold.otf"; sourceTree = "<group>"; }; CDC3F85B269471C900AAE3BF /* SF-Pro-Display-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Bold.otf"; sourceTree = "<group>"; };
CDE43FD52695C65700DBE79E /* HourlyTemps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HourlyTemps.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
...@@ -149,12 +153,14 @@ ...@@ -149,12 +153,14 @@
CD259C12268DE1F7008D205E /* Widgets */ = { CD259C12268DE1F7008D205E /* Widgets */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
CDE43FD52695C65700DBE79E /* HourlyTemps.swift */,
CD7D316E268EF2BC000D01FA /* WidgetViewModel.swift */, CD7D316E268EF2BC000D01FA /* WidgetViewModel.swift */,
CD7D3177268EFB9E000D01FA /* WidgetViewModelMock.swift */, CD7D3177268EFB9E000D01FA /* WidgetViewModelMock.swift */,
CD7D3163268EEF56000D01FA /* SmallTemperatureWidgetView.swift */, CD7D3163268EEF56000D01FA /* SmallTemperatureWidgetView.swift */,
CD3C83C226933ABC0087A225 /* MediumTemperatureWidgetView.swift */, CD3C83C226933ABC0087A225 /* MediumTemperatureWidgetView.swift */,
CD7D315E268EEF37000D01FA /* SharedViews */, CD7D315E268EEF37000D01FA /* SharedViews */,
CD7D3175268EF8A9000D01FA /* WidgetFont.swift */, CD7D3175268EF8A9000D01FA /* WidgetFont.swift */,
CDC2E75526970D4A00B410D4 /* MediumWidgetViewModel.swift */,
); );
path = Widgets; path = Widgets;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -342,12 +348,14 @@ ...@@ -342,12 +348,14 @@
files = ( files = (
CD7D3162268EEF49000D01FA /* HighLowTemperatureView.swift in Sources */, CD7D3162268EEF49000D01FA /* HighLowTemperatureView.swift in Sources */,
CD4B95F6269494DA0061961F /* LineShape.swift in Sources */, CD4B95F6269494DA0061961F /* LineShape.swift in Sources */,
CDC2E75626970D4A00B410D4 /* MediumWidgetViewModel.swift in Sources */,
CD7D3161268EEF49000D01FA /* CityNameView.swift in Sources */, CD7D3161268EEF49000D01FA /* CityNameView.swift in Sources */,
CD7D3164268EEF56000D01FA /* SmallTemperatureWidgetView.swift in Sources */, CD7D3164268EEF56000D01FA /* SmallTemperatureWidgetView.swift in Sources */,
CD3C83C326933ABD0087A225 /* MediumTemperatureWidgetView.swift in Sources */, CD3C83C326933ABD0087A225 /* MediumTemperatureWidgetView.swift in Sources */,
CD7D3168268EF167000D01FA /* UIFont+Font.swift in Sources */, CD7D3168268EF167000D01FA /* UIFont+Font.swift in Sources */,
CD7D3187268F1F2E000D01FA /* UIImage+Resize.swift in Sources */, CD7D3187268F1F2E000D01FA /* UIImage+Resize.swift in Sources */,
CD7D3178268EFB9E000D01FA /* WidgetViewModelMock.swift in Sources */, CD7D3178268EFB9E000D01FA /* WidgetViewModelMock.swift in Sources */,
CDE43FD62695C65700DBE79E /* HourlyTemps.swift in Sources */,
CD7D316F268EF2BC000D01FA /* WidgetViewModel.swift in Sources */, CD7D316F268EF2BC000D01FA /* WidgetViewModel.swift in Sources */,
CD7D3182268F0C60000D01FA /* OneWeatherUI+Global.swift in Sources */, CD7D3182268F0C60000D01FA /* OneWeatherUI+Global.swift in Sources */,
CDC3F85A26946D0700AAE3BF /* HourlyView.swift in Sources */, CDC3F85A26946D0700AAE3BF /* HourlyView.swift in Sources */,
......
//
// HourlyTemps.swift
// OneWeatherUI
//
// Created by Dmitry Stepanets on 07.07.2021.
//
import Foundation
struct HourlyTemps {
let currentTemp: Int
let maxTemp: Int
let lowTemp: Int
init(currentTemp: Int, temps: [Int]) {
self.currentTemp = currentTemp
self.maxTemp = temps.sorted{$0 > $1}.first ?? 0
self.lowTemp = temps.sorted{$0 < $1}.first ?? 0
}
}
...@@ -8,6 +8,16 @@ ...@@ -8,6 +8,16 @@
import SwiftUI import SwiftUI
import WidgetKit import WidgetKit
private struct FramePreferenceKey: PreferenceKey {
typealias Value = CGRect
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
@available (iOS 14, *) @available (iOS 14, *)
public struct MediumTemperatureWidgetView: View { public struct MediumTemperatureWidgetView: View {
//Public //Public
...@@ -18,6 +28,9 @@ public struct MediumTemperatureWidgetView: View { ...@@ -18,6 +28,9 @@ public struct MediumTemperatureWidgetView: View {
} }
//Private //Private
private let temps = [25, 23, 19, 32]
@ObservedObject private var viewModel = MediumWidgetViewModel()
@State private var hourlyStackFrame: CGRect = .zero
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
public var body: some View { public var body: some View {
...@@ -67,23 +80,35 @@ public struct MediumTemperatureWidgetView: View { ...@@ -67,23 +80,35 @@ public struct MediumTemperatureWidgetView: View {
} }
} }
ZStack { GeometryReader { geo in
HStack(alignment: .center, spacing: 10) { ZStack {
Spacer() HStack(spacing: 10) {
HourlyView() ForEach(0 ..< temps.count) { index in
HourlyView() HourlyView(hourlyTemps: .init(currentTemp: temps[index],
HourlyView() temps: temps),
HourlyView() viewIndex: index)
Spacer() .frame(maxWidth: 70)
}
}
.overlay(
GeometryReader { geoInner in
Color(.clear)
.onAppear {
hourlyStackFrame = geoInner.frame(in: .global)
}
}
)
LineShape(temps: temps)
.stroke(Color.red,
style: .init(lineWidth: 3,
lineCap: .round,
lineJoin: .round))
.frame(width: hourlyStackFrame.width, height: 22)
} }
LineShape(lineHeight: 30) .frame(width: geo.frame(in: .local).width)
.stroke(Color.red,
style: .init(lineWidth: 3,
lineCap: .round,
lineJoin: .round))
.background(Color(white: 1, opacity: 0.3))
.frame(height: 30)
} }
} }
.padding([.leading, .trailing], 10) .padding([.leading, .trailing], 10)
.background(Color("PrimaryBackground", .background(Color("PrimaryBackground",
...@@ -94,6 +119,15 @@ public struct MediumTemperatureWidgetView: View { ...@@ -94,6 +119,15 @@ public struct MediumTemperatureWidgetView: View {
@available(iOS 14, *) @available(iOS 14, *)
private extension View { private extension View {
func framePreference(coordinateSpace: CoordinateSpace) -> some View {
background(
GeometryReader {
Color.clear.preference(key: FramePreferenceKey.self,
value: $0.frame(in: coordinateSpace))
}
)
}
func shadow(for colorScheme: ColorScheme) -> some View { func shadow(for colorScheme: ColorScheme) -> some View {
switch colorScheme { switch colorScheme {
case .light: case .light:
......
//
// MediumWidgetViewModel.swift
// OneWeatherUI
//
// Created by Dmitry Stepanets on 08.07.2021.
//
import SwiftUI
@available(iOS 14, *)
final class MediumWidgetViewModel: ObservableObject {
@Published var hourlyViewFrames = [CGRect]()
func add(rect: CGRect) {
self.hourlyViewFrames.append(rect)
self.hourlyViewFrames.sort{$0.origin.x < $1.origin.x}
objectWillChange.send()
}
}
...@@ -7,48 +7,133 @@ ...@@ -7,48 +7,133 @@
import SwiftUI import SwiftUI
private struct FramePreferenceKey: PreferenceKey {
typealias Value = CGRect
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
private struct PointOriginPreferenceKey: PreferenceKey {
typealias Value = CGPoint
static var defaultValue: CGPoint = .zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {
value = nextValue()
}
}
@available(iOS 14, *) @available(iOS 14, *)
struct HourlyView: View { struct HourlyView: View {
//Private //Private
@State private var viewFrame = CGRect.zero
@State private var tempLabelFrame = CGRect.zero
@State private var timeLabelFrame = CGRect.zero
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
private let hourlyTemps: HourlyTemps
init() { //Computed
OneWeatherUI.loadFonts private var lineAreaRect: CGRect {
return .init(x: 0,
y: tempLabelFrame.origin.y + tempLabelFrame.height + 4,
width: viewFrame.width,
height: tempLabelFrame.origin.y + tempLabelFrame.height - timeLabelFrame.origin.y - 4)
}
private var calcPointOriginY: CGFloat {
let diff = hourlyTemps.maxTemp - hourlyTemps.lowTemp
let currentMultiply = CGFloat(hourlyTemps.maxTemp - hourlyTemps.currentTemp) / CGFloat(diff)
return lineAreaRect.height * CGFloat(currentMultiply) + lineAreaRect.origin.y
}
//Public
init(hourlyTemps: HourlyTemps, viewIndex: Int) {
self.hourlyTemps = hourlyTemps
} }
var body: some View { var body: some View {
VStack { GeometryReader { viewGeo in
Text("25") ZStack {
.foregroundColor(Color("PrimaryTextColor", VStack {
bundle: OneWeatherUI.frameworkBundle)) Text("\(hourlyTemps.currentTemp)" + "°")
.font(WidgetFont.SFProDisplay.bold(size: 16).font) .foregroundColor(Color("PrimaryTextColor",
.padding(.top, 8) bundle: OneWeatherUI.frameworkBundle))
Spacer() .font(WidgetFont.SFProDisplay.bold(size: 16).font)
Text("NOW") .padding(.top, 8)
.foregroundColor(Color("PrimaryTextColor", .framePreference()
bundle: OneWeatherUI.frameworkBundle)) .onPreferenceChange(FramePreferenceKey.self) { value in
.font(WidgetFont.SFProDisplay.bold(size: 12).font) self.tempLabelFrame = value
.padding(.bottom, 8) }
Spacer()
Text("NOW")
.foregroundColor(Color("PrimaryTextColor",
bundle: OneWeatherUI.frameworkBundle))
.font(WidgetFont.SFProDisplay.bold(size: 12).font)
.padding(.bottom, 8)
.framePreference()
.onPreferenceChange(FramePreferenceKey.self, perform: { value in
self.timeLabelFrame = value
})
}
.frame(width: 70)
.background(Color("HourlyContainerBackground",
bundle: OneWeatherUI.frameworkBundle))
.compositingGroup()
.cornerRadius(12)
.primaryShadow(for: colorScheme)
.secondaryShadow(for: colorScheme)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color("HourlyContainerBorder",
bundle: OneWeatherUI.frameworkBundle),
lineWidth: 1)
)
.preference(key: FramePreferenceKey.self, value: viewGeo.frame(in: .local))
.onPreferenceChange(FramePreferenceKey.self, perform: { value in
self.viewFrame = value
})
} }
.frame(minWidth: 70) .position(x: viewFrame.width / 2, y: viewFrame.height / 2)
.background(Color("HourlyContainerBackground",
bundle: OneWeatherUI.frameworkBundle)) Circle()
.compositingGroup() .overlay(
.cornerRadius(12) Circle()
.primaryShadow(for: colorScheme) .stroke(Color.blue, lineWidth: 2)
.secondaryShadow(for: colorScheme) )
.overlay( .frame(width: 5, height: 5)
RoundedRectangle(cornerRadius: 12) .foregroundColor(.white)
.stroke(Color("HourlyContainerBorder", .position(x: viewFrame.width / 2,
bundle: OneWeatherUI.frameworkBundle), y: calcPointOriginY)
lineWidth: 1) }
)
} }
} }
//MARK:- UI helpers //MARK:- UI helpers
@available(iOS 14, *) @available(iOS 14, *)
private extension View { private extension View {
func framePreference() -> some View {
background(
GeometryReader {
Color.clear.preference(key: FramePreferenceKey.self,
value: $0.frame(in: .local))
}
)
}
func originPreference() -> some View {
background(
GeometryReader {
Color.clear.preference(key: PointOriginPreferenceKey.self,
value: $0.frame(in: .local).origin)
}
)
}
func primaryShadow(for colorScheme: ColorScheme) -> some View { func primaryShadow(for colorScheme: ColorScheme) -> some View {
switch colorScheme { switch colorScheme {
case .light: case .light:
...@@ -83,12 +168,3 @@ private extension View { ...@@ -83,12 +168,3 @@ private extension View {
} }
} }
} }
//MARK:- Preview
@available(iOS 14, *)
struct HourlyView_Preview: PreviewProvider {
static var previews: some View {
HourlyView()
.frame(width: 70, height: 78)
}
}
...@@ -7,18 +7,42 @@ ...@@ -7,18 +7,42 @@
import SwiftUI import SwiftUI
@available(iOS 14, *)
struct LineShape: Shape { struct LineShape: Shape {
private let lineHeight: Int //Private
private let temps = [20, 23, 15, 29] private let kViewWidth: CGFloat = 70
private let kInset: CGFloat = 0
init(lineHeight: Int) { //Public
self.lineHeight = lineHeight let temps: [Int]
}
func path(in rect: CGRect) -> Path { func path(in rect: CGRect) -> Path {
var line = Path() var line = Path()
line.move(to: CGPoint(x: rect.minX, y: CGFloat(lineHeight / 2)))
line.addLine(to: CGPoint(x: rect.maxX, y: CGFloat(lineHeight / 2))) let maxTemp = temps.sorted{$0 > $1}.first ?? 0
let minTemp = temps.sorted{$0 < $1}.first ?? 0
let diff = maxTemp - minTemp
line.move(to: .init(x: kInset, y: rect.height / 2))
var points = [CGPoint]()
for index in 0..<temps.count {
let multiply = CGFloat(maxTemp - temps[index]) / CGFloat(diff)
let space:CGFloat = index > 0 && index <= 3 ? 10 : 0
if index == 0 {
points.append(.init(x: kViewWidth / 2, y: rect.height * multiply))
}
else {
let xOffset = (kViewWidth + space) * CGFloat(index) + kViewWidth / 2
points.append(.init(x: xOffset, y: rect.height * multiply))
}
}
points.append(.init(x: rect.width - kInset, y: rect.height / 2))
for index in 0..<points.count {
line.addLine(to: points[index])
}
return line return line
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment