Commit 3abd9e14 by Dmitriy Stepanets

Working on graph line layout

parent 3cc5019d
......@@ -90,7 +90,7 @@
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetFamily"
value = "small"
value = "medium"
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
......
......@@ -27,8 +27,10 @@
CD7D3180268F04DD000D01FA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD7D316C268EF219000D01FA /* Assets.xcassets */; };
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 */; };
CDC2E75626970D4A00B410D4 /* MediumWidgetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC2E75526970D4A00B410D4 /* MediumWidgetViewModel.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 */; };
CDE43FD62695C65700DBE79E /* HourlyTemps.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE43FD52695C65700DBE79E /* HourlyTemps.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
......@@ -69,8 +71,10 @@
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>"; };
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>"; };
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 */
/* Begin PBXFrameworksBuildPhase section */
......@@ -149,12 +153,14 @@
CD259C12268DE1F7008D205E /* Widgets */ = {
isa = PBXGroup;
children = (
CDE43FD52695C65700DBE79E /* HourlyTemps.swift */,
CD7D316E268EF2BC000D01FA /* WidgetViewModel.swift */,
CD7D3177268EFB9E000D01FA /* WidgetViewModelMock.swift */,
CD7D3163268EEF56000D01FA /* SmallTemperatureWidgetView.swift */,
CD3C83C226933ABC0087A225 /* MediumTemperatureWidgetView.swift */,
CD7D315E268EEF37000D01FA /* SharedViews */,
CD7D3175268EF8A9000D01FA /* WidgetFont.swift */,
CDC2E75526970D4A00B410D4 /* MediumWidgetViewModel.swift */,
);
path = Widgets;
sourceTree = "<group>";
......@@ -342,12 +348,14 @@
files = (
CD7D3162268EEF49000D01FA /* HighLowTemperatureView.swift in Sources */,
CD4B95F6269494DA0061961F /* LineShape.swift in Sources */,
CDC2E75626970D4A00B410D4 /* MediumWidgetViewModel.swift in Sources */,
CD7D3161268EEF49000D01FA /* CityNameView.swift in Sources */,
CD7D3164268EEF56000D01FA /* SmallTemperatureWidgetView.swift in Sources */,
CD3C83C326933ABD0087A225 /* MediumTemperatureWidgetView.swift in Sources */,
CD7D3168268EF167000D01FA /* UIFont+Font.swift in Sources */,
CD7D3187268F1F2E000D01FA /* UIImage+Resize.swift in Sources */,
CD7D3178268EFB9E000D01FA /* WidgetViewModelMock.swift in Sources */,
CDE43FD62695C65700DBE79E /* HourlyTemps.swift in Sources */,
CD7D316F268EF2BC000D01FA /* WidgetViewModel.swift in Sources */,
CD7D3182268F0C60000D01FA /* OneWeatherUI+Global.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 @@
import SwiftUI
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, *)
public struct MediumTemperatureWidgetView: View {
//Public
......@@ -18,6 +28,9 @@ public struct MediumTemperatureWidgetView: View {
}
//Private
private let temps = [25, 23, 19, 32]
@ObservedObject private var viewModel = MediumWidgetViewModel()
@State private var hourlyStackFrame: CGRect = .zero
@Environment(\.colorScheme) private var colorScheme
public var body: some View {
......@@ -67,23 +80,35 @@ public struct MediumTemperatureWidgetView: View {
}
}
ZStack {
HStack(alignment: .center, spacing: 10) {
Spacer()
HourlyView()
HourlyView()
HourlyView()
HourlyView()
Spacer()
GeometryReader { geo in
ZStack {
HStack(spacing: 10) {
ForEach(0 ..< temps.count) { index in
HourlyView(hourlyTemps: .init(currentTemp: temps[index],
temps: temps),
viewIndex: index)
.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)
.stroke(Color.red,
style: .init(lineWidth: 3,
lineCap: .round,
lineJoin: .round))
.background(Color(white: 1, opacity: 0.3))
.frame(height: 30)
.frame(width: geo.frame(in: .local).width)
}
}
.padding([.leading, .trailing], 10)
.background(Color("PrimaryBackground",
......@@ -94,6 +119,15 @@ public struct MediumTemperatureWidgetView: View {
@available(iOS 14, *)
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 {
switch colorScheme {
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 @@
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, *)
struct HourlyView: View {
//Private
@State private var viewFrame = CGRect.zero
@State private var tempLabelFrame = CGRect.zero
@State private var timeLabelFrame = CGRect.zero
@Environment(\.colorScheme) private var colorScheme
private let hourlyTemps: HourlyTemps
init() {
OneWeatherUI.loadFonts
//Computed
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 {
VStack {
Text("25")
.foregroundColor(Color("PrimaryTextColor",
bundle: OneWeatherUI.frameworkBundle))
.font(WidgetFont.SFProDisplay.bold(size: 16).font)
.padding(.top, 8)
Spacer()
Text("NOW")
.foregroundColor(Color("PrimaryTextColor",
bundle: OneWeatherUI.frameworkBundle))
.font(WidgetFont.SFProDisplay.bold(size: 12).font)
.padding(.bottom, 8)
GeometryReader { viewGeo in
ZStack {
VStack {
Text("\(hourlyTemps.currentTemp)" + "°")
.foregroundColor(Color("PrimaryTextColor",
bundle: OneWeatherUI.frameworkBundle))
.font(WidgetFont.SFProDisplay.bold(size: 16).font)
.padding(.top, 8)
.framePreference()
.onPreferenceChange(FramePreferenceKey.self) { value in
self.tempLabelFrame = value
}
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)
.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)
)
.position(x: viewFrame.width / 2, y: viewFrame.height / 2)
Circle()
.overlay(
Circle()
.stroke(Color.blue, lineWidth: 2)
)
.frame(width: 5, height: 5)
.foregroundColor(.white)
.position(x: viewFrame.width / 2,
y: calcPointOriginY)
}
}
}
//MARK:- UI helpers
@available(iOS 14, *)
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 {
switch colorScheme {
case .light:
......@@ -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 @@
import SwiftUI
@available(iOS 14, *)
struct LineShape: Shape {
private let lineHeight: Int
private let temps = [20, 23, 15, 29]
//Private
private let kViewWidth: CGFloat = 70
private let kInset: CGFloat = 0
init(lineHeight: Int) {
self.lineHeight = lineHeight
}
//Public
let temps: [Int]
func path(in rect: CGRect) -> 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
}
......
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