Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
1
1weather
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Dmitriy Stepanets
1weather
Commits
bb0e57d7
Commit
bb0e57d7
authored
Mar 23, 2021
by
Dmitriy Stepanets
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Finished ForecastViewController
parent
a0b2dc91
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
460 additions
and
56 deletions
+460
-56
1Weather.xcodeproj/project.pbxproj
+8
-0
1Weather.xcodeproj/xcuserdata/dstepanets.xcuserdatad/xcschemes/xcschememanagement.plist
+1
-1
1Weather.xcworkspace/xcuserdata/dstepanets.xcuserdatad/UserInterfaceState.xcuserstate
+0
-0
1Weather/Extensions/Calendar+TimeZone.swift
+7
-0
1Weather/Model/HelperTypes.swift
+37
-0
1Weather/Resources/Assets.xcassets/wind_direction.imageset/Contents.json
+16
-0
1Weather/Resources/Assets.xcassets/wind_direction.imageset/wind_direction.pdf
+0
-0
1Weather/UI/Helpers/AppFont.swift
+18
-20
1Weather/UI/Helpers/ForecastTimePeriod/ForecastPeriodButton.swift
+7
-2
1Weather/UI/Helpers/ForecastTimePeriod/ForecastTimePeriodView.swift
+49
-12
1Weather/UI/Helpers/ForecastTimePeriod/ForecastWindButton.swift
+163
-0
1Weather/UI/Helpers/ForecastTimePeriod/GraphLine.swift
+8
-7
1Weather/UI/Helpers/ForecastTimePeriod/GraphLineSettings.swift
+1
-0
1Weather/UI/Helpers/ForecastTimePeriod/GraphView.swift
+27
-0
1Weather/UI/View controllers/Forecast/Cells/ForecastCellFactory.swift
+8
-1
1Weather/UI/View controllers/Forecast/Cells/ForecastDailyCell.swift
+1
-1
1Weather/UI/View controllers/Forecast/Cells/ForecastHourlyCell.swift
+1
-1
1Weather/UI/View controllers/Forecast/Cells/ForecastWindSpeedCell.swift
+100
-0
1Weather/UI/View controllers/Today/Cells/CityForecastTimePeriodCell.swift
+2
-1
1Weather/UI/View controllers/Today/Cells/CityPrecipCell/PrecipButton.swift
+5
-9
Pods/Pods.xcodeproj/xcuserdata/dstepanets.xcuserdatad/xcschemes/xcschememanagement.plist
+1
-1
No files found.
1Weather.xcodeproj/project.pbxproj
View file @
bb0e57d7
...
...
@@ -77,6 +77,8 @@
CDE18DCD25D1666700C80ED9
/* ForecastCoordinator.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
CDE18DCC25D1666700C80ED9
/* ForecastCoordinator.swift */
;
};
CDE18DD125D166F900C80ED9
/* ForecastViewController.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
CDE18DD025D166F900C80ED9
/* ForecastViewController.swift */
;
};
CDE18DD825D16CB200C80ED9
/* NavigationCityButton.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
CDE18DD725D16CB200C80ED9
/* NavigationCityButton.swift */
;
};
CDE2BF222609D4250085C930
/* ForecastWindSpeedCell.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
CDE2BF212609D4250085C930
/* ForecastWindSpeedCell.swift */
;
};
CDE2BF252609D9140085C930
/* ForecastWindButton.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
CDE2BF242609D9140085C930
/* ForecastWindButton.swift */
;
};
CDEE8AD725DA882200C289DE
/* ForecastPeriodButton.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
CDEE8AD625DA882200C289DE
/* ForecastPeriodButton.swift */
;
};
CE578FD325F7E89400E8B85D
/* DayTimeWeather.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
CE578FD225F7E89400E8B85D
/* DayTimeWeather.swift */
;
};
CE9D181625ECB8370028D9D7
/* MulticastDelegate.swift in Sources */
=
{
isa
=
PBXBuildFile
;
fileRef
=
CE9D181525ECB8370028D9D7
/* MulticastDelegate.swift */
;
};
...
...
@@ -178,6 +180,8 @@
CDE18DCC25D1666700C80ED9
/* ForecastCoordinator.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
path
=
ForecastCoordinator.swift
;
sourceTree
=
"<group>"
;
};
CDE18DD025D166F900C80ED9
/* ForecastViewController.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
path
=
ForecastViewController.swift
;
sourceTree
=
"<group>"
;
};
CDE18DD725D16CB200C80ED9
/* NavigationCityButton.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
path
=
NavigationCityButton.swift
;
sourceTree
=
"<group>"
;
};
CDE2BF212609D4250085C930
/* ForecastWindSpeedCell.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
path
=
ForecastWindSpeedCell.swift
;
sourceTree
=
"<group>"
;
};
CDE2BF242609D9140085C930
/* ForecastWindButton.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
path
=
ForecastWindButton.swift
;
sourceTree
=
"<group>"
;
};
CDEE8AD625DA882200C289DE
/* ForecastPeriodButton.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
path
=
ForecastPeriodButton.swift
;
sourceTree
=
"<group>"
;
};
CE578FD225F7E89400E8B85D
/* DayTimeWeather.swift */
=
{
isa
=
PBXFileReference
;
lastKnownFileType
=
sourcecode.swift
;
path
=
DayTimeWeather.swift
;
sourceTree
=
"<group>"
;
};
CE9D181525ECB8370028D9D7
/* MulticastDelegate.swift */
=
{
isa
=
PBXFileReference
;
fileEncoding
=
4
;
lastKnownFileType
=
sourcecode.swift
;
path
=
MulticastDelegate.swift
;
sourceTree
=
"<group>"
;
};
...
...
@@ -237,6 +241,7 @@
CD3F6E6825FA59D4002DB99B
/* ForecastDetailPeriodButton.swift */
,
CD3F6E6B25FA5A90002DB99B
/* PeriodButtonProtocol.swift */
,
CD593BC126088A5900C93428
/* TimePeriodOffsetHolder.swift */
,
CDE2BF242609D9140085C930
/* ForecastWindButton.swift */
,
);
path
=
ForecastTimePeriod
;
sourceTree
=
"<group>"
;
...
...
@@ -435,6 +440,7 @@
CD593BCB2608A4F200C93428
/* ForecastDailyCell.swift */
,
CD593BCE2608A50900C93428
/* ForecastHourlyCell.swift */
,
CD593BD22608BC3F00C93428
/* ForecastDayCell.swift */
,
CDE2BF212609D4250085C930
/* ForecastWindSpeedCell.swift */
,
);
path
=
Cells
;
sourceTree
=
"<group>"
;
...
...
@@ -721,6 +727,7 @@
CD82300325D69DE400A05501
/* CityConditionsCell.swift in Sources */
,
CEC526FD25E795F700DA58A5
/* WdtWeatherSource.swift in Sources */
,
CEAFF09225DFC71D00DF4EBF
/* HelperTypes.swift in Sources */
,
CDE2BF222609D4250085C930
/* ForecastWindSpeedCell.swift in Sources */
,
CDA5543025EFA13F00A2E08C
/* Measurement+String.swift in Sources */
,
CE9D181925ECB9A70028D9D7
/* Logger.swift in Sources */
,
CE578FD325F7E89400E8B85D
/* DayTimeWeather.swift in Sources */
,
...
...
@@ -783,6 +790,7 @@
CD15DB4225DA806C00024727
/* CityForecastTimePeriodCell.swift in Sources */
,
CEC5276025E92DDA00DA58A5
/* WdtHourlySummary.swift in Sources */
,
CDE18DCA25D165F100C80ED9
/* UITabBarController+Append.swift in Sources */
,
CDE2BF252609D9140085C930
/* ForecastWindButton.swift in Sources */
,
CD251ED82603633800ED7A65
/* ForecastPrecipitationCell.swift in Sources */
,
CD86246125E662BC0097F3FB
/* SunUvLineView.swift in Sources */
,
CEC526FA25E7959A00DA58A5
/* WeatherSource.swift in Sources */
,
...
...
1Weather.xcodeproj/xcuserdata/dstepanets.xcuserdatad/xcschemes/xcschememanagement.plist
View file @
bb0e57d7
...
...
@@ -7,7 +7,7 @@
<
k
e
y
>
1Weather.xcscheme_
^#
shared
#^
_
<
/k
e
y
>
<
d
i
c
t
>
<
k
e
y
>
orderHint
<
/k
e
y
>
<
int
e
g
e
r
>
6
<
/int
e
g
e
r
>
<
int
e
g
e
r
>
5
<
/int
e
g
e
r
>
<
/
d
i
c
t
>
<
k
e
y
>
PG
(
Playground
)
1.xcscheme
<
/k
e
y
>
<
d
i
c
t
>
...
...
1Weather.xcworkspace/xcuserdata/dstepanets.xcuserdatad/UserInterfaceState.xcuserstate
View file @
bb0e57d7
No preview for this file type
1Weather/Extensions/Calendar+TimeZone.swift
View file @
bb0e57d7
...
...
@@ -13,4 +13,11 @@ extension Calendar {
cal
.
timeZone
=
timeZone
return
cal
}
static
func
isNow
(
fromDate
date
:
Date
,
timeZone
:
TimeZone
)
->
Bool
{
guard
let
nowDate
=
Date
.
nowDate
(
timeZone
:
timeZone
)
else
{
return
false
}
return
self
.
timeZoneCalendar
(
timeZone
:
timeZone
)
.
isDate
(
date
,
equalTo
:
nowDate
,
toGranularity
:
.
hour
)
}
}
1Weather/Model/HelperTypes.swift
View file @
bb0e57d7
...
...
@@ -115,6 +115,43 @@ public enum WindDirection: String, Codable, CaseIterable {
case
westNorthWest
=
"WNW"
case
northWest
=
"NW"
case
northNorthWest
=
"NNW"
var
degrees
:
CGFloat
{
switch
self
{
case
.
north
:
return
0
case
.
northNorthEast
:
return
22.5
case
.
northEast
:
return
45
case
.
eastNorthEast
:
return
67.5
case
.
east
:
return
90
case
.
eastSouthEast
:
return
112.5
case
.
southEast
:
return
135
case
.
southSouthEast
:
return
157.5
case
.
south
:
return
180
case
.
southSouthWest
:
return
202.5
case
.
southWest
:
return
225
case
.
westSouthWest
:
return
247.5
case
.
west
:
return
270
case
.
westNorthWest
:
return
292.5
case
.
northWest
:
return
315
case
.
northNorthWest
:
return
337.5
}
}
}
...
...
1Weather/Resources/Assets.xcassets/wind_direction.imageset/Contents.json
0 → 100644
View file @
bb0e57d7
{
"images"
:
[
{
"filename"
:
"wind_direction.pdf"
,
"idiom"
:
"universal"
}
],
"info"
:
{
"author"
:
"xcode"
,
"version"
:
1
},
"properties"
:
{
"preserves-vector-representation"
:
true
,
"template-rendering-intent"
:
"template"
}
}
1Weather/Resources/Assets.xcassets/wind_direction.imageset/wind_direction.pdf
0 → 100644
View file @
bb0e57d7
File added
1Weather/UI/Helpers/AppFont.swift
View file @
bb0e57d7
...
...
@@ -8,37 +8,35 @@
import
UIKit
public
struct
AppFont
{
private
static
func
fontDescriptor
(
size
:
CGFloat
,
weight
:
UIFont
.
Weight
)
->
UIFontDescriptor
{
let
traitsDict
=
[
UIFontDescriptor
.
TraitKey
.
weight
:
weight
]
let
descriptor
=
UIFontDescriptor
(
fontAttributes
:
[
UIFontDescriptor
.
AttributeName
.
size
:
size
,
UIFontDescriptor
.
AttributeName
.
family
:
"SF Pro"
,
UIFontDescriptor
.
AttributeName
.
traits
:
traitsDict
])
return
descriptor
}
public
struct
SFPro
{
static
func
regular
(
size
:
CGFloat
)
->
UIFont
{
guard
let
font
=
UIFont
(
name
:
"SFPro-Regualr"
,
size
:
size
)
else
{
return
UIFont
.
systemFont
(
ofSize
:
size
)
}
return
font
let
descriptor
=
AppFont
.
fontDescriptor
(
size
:
size
,
weight
:
.
regular
)
return
UIFont
(
descriptor
:
descriptor
,
size
:
size
)
}
static
func
bold
(
size
:
CGFloat
)
->
UIFont
{
guard
let
font
=
UIFont
(
name
:
"SFPro-Bold"
,
size
:
size
)
else
{
return
UIFont
.
systemFont
(
ofSize
:
size
,
weight
:
.
bold
)
}
return
font
let
descriptor
=
AppFont
.
fontDescriptor
(
size
:
size
,
weight
:
.
bold
)
return
UIFont
(
descriptor
:
descriptor
,
size
:
size
)
}
static
func
medium
(
size
:
CGFloat
)
->
UIFont
{
guard
let
font
=
UIFont
(
name
:
"SFPro-Medium"
,
size
:
size
)
else
{
return
UIFont
.
systemFont
(
ofSize
:
size
,
weight
:
.
bold
)
}
return
font
let
descriptor
=
AppFont
.
fontDescriptor
(
size
:
size
,
weight
:
.
medium
)
return
UIFont
(
descriptor
:
descriptor
,
size
:
size
)
}
static
func
semibold
(
size
:
CGFloat
)
->
UIFont
{
guard
let
font
=
UIFont
(
name
:
"SFPro-Semibold"
,
size
:
size
)
else
{
return
UIFont
.
systemFont
(
ofSize
:
size
,
weight
:
.
semibold
)
}
return
font
let
descriptor
=
AppFont
.
fontDescriptor
(
size
:
size
,
weight
:
.
semibold
)
return
UIFont
(
descriptor
:
descriptor
,
size
:
size
)
}
}
}
1Weather/UI/Helpers/ForecastTimePeriod/ForecastPeriodButton.swift
View file @
bb0e57d7
...
...
@@ -102,8 +102,13 @@ class ForecastPeriodButton: UIControl, PeriodButtonProtocol {
self
.
tempLabel
.
text
=
hourlyWeather
.
temp
?
.
shortString
self
.
minTempLabel
.
text
=
nil
self
.
indicatorImageView
.
image
=
nil
ForecastPeriodButton
.
hourlyFormatter
.
timeZone
=
hourlyWeather
.
timeZone
self
.
timeLabel
.
text
=
ForecastPeriodButton
.
hourlyFormatter
.
string
(
from
:
hourlyWeather
.
date
)
if
Calendar
.
isNow
(
fromDate
:
hourlyWeather
.
date
,
timeZone
:
hourlyWeather
.
timeZone
)
{
self
.
timeLabel
.
text
=
"day.now"
.
localized
()
.
uppercased
()
}
else
{
ForecastPeriodButton
.
hourlyFormatter
.
timeZone
=
hourlyWeather
.
timeZone
self
.
timeLabel
.
text
=
ForecastPeriodButton
.
hourlyFormatter
.
string
(
from
:
hourlyWeather
.
date
)
}
}
}
...
...
1Weather/UI/Helpers/ForecastTimePeriod/ForecastTimePeriodView.swift
View file @
bb0e57d7
...
...
@@ -16,6 +16,12 @@ private struct HourlyGraphPoints {
let
points
:
[
CGPoint
]
}
public
enum
ForecastType
{
case
daily
case
hourly
case
wind
}
protocol
ForecastTimePeriodViewDelegate
:
class
{
func
forecastTimePeriodView
(
view
:
ForecastTimePeriodView
,
didSelectButtonAt
index
:
Int
)
func
offsetDidChange
(
offset
:
CGFloat
)
...
...
@@ -27,7 +33,7 @@ class ForecastTimePeriodView: UIView {
private
let
stackView
=
UIStackView
()
private
let
graphView
=
GraphView
()
private
var
graphRect
:
CGRect
=
.
zero
private
var
current
TimePeriod
=
TimePeriod
.
daily
private
var
current
ForecastType
=
ForecastType
.
daily
private
var
dailyGraphPoints
=
DailyGraphPoints
(
maxTempPoints
:
[
CGPoint
](),
minTempPoints
:
[
CGPoint
]())
private
var
hourlyGraphPoints
=
HourlyGraphPoints
(
points
:
[
CGPoint
]())
private
var
daily
=
[
DailyWeather
]()
...
...
@@ -64,8 +70,8 @@ class ForecastTimePeriodView: UIView {
}
}
public
func
set
(
timePeriod
:
TimePeriod
,
buttonType
:
PeriodButtonProtocol
.
Type
)
{
self
.
current
TimePeriod
=
timePeriod
public
func
set
(
forecastType
:
ForecastType
,
buttonType
:
PeriodButtonProtocol
.
Type
)
{
self
.
current
ForecastType
=
forecastType
rebuildButtons
(
buttonType
:
buttonType
)
}
...
...
@@ -103,19 +109,19 @@ class ForecastTimePeriodView: UIView {
stackView
.
removeAll
()
let
numberOfButtons
:
Int
switch
current
TimePeriod
{
switch
current
ForecastType
{
case
.
daily
:
numberOfButtons
=
daily
.
count
case
.
hourly
:
case
.
hourly
,
.
wind
:
numberOfButtons
=
hourly
.
count
}
for
index
in
0
..<
numberOfButtons
{
let
forecastButton
=
buttonType
.
init
()
switch
current
TimePeriod
{
switch
current
ForecastType
{
case
.
daily
:
forecastButton
.
configure
(
dailyWeather
:
daily
[
index
])
case
.
hourly
:
case
.
hourly
,
.
wind
:
forecastButton
.
configure
(
hourlyWeather
:
hourly
[
index
])
}
forecastButton
.
index
=
index
...
...
@@ -145,11 +151,13 @@ class ForecastTimePeriodView: UIView {
}
private
func
updateGraphPoints
()
{
switch
current
TimePeriod
{
switch
current
ForecastType
{
case
.
daily
:
updateDailyGraphPoints
()
case
.
hourly
:
updateHourlyGraphPoints
()
case
.
wind
:
updateWindPoints
()
}
}
...
...
@@ -196,7 +204,6 @@ class ForecastTimePeriodView: UIView {
let
temps
=
(
hourly
.
map
{
CGFloat
(
$0
.
temp
?
.
localeValue
??
0
)
})
let
maxTemp
=
temps
.
max
()
??
0
var
points
=
[
CGPoint
]()
for
index
in
0
..<
hoursCount
{
guard
let
stackButton
=
stackView
.
arrangedSubviews
[
index
]
as?
PeriodButtonProtocol
else
{
continue
}
...
...
@@ -218,6 +225,32 @@ class ForecastTimePeriodView: UIView {
hourlyGraphPoints
=
HourlyGraphPoints
(
points
:
points
)
}
private
func
updateWindPoints
()
{
let
hoursCount
=
hourly
.
count
let
speeds
=
(
hourly
.
map
{
CGFloat
(
$0
.
windSpeed
?
.
value
??
0
)
})
let
maxWindSpeed
=
speeds
.
max
()
??
0
var
points
=
[
CGPoint
]()
for
index
in
0
..<
hoursCount
{
guard
let
stackButton
=
stackView
.
arrangedSubviews
[
index
]
as?
PeriodButtonProtocol
else
{
continue
}
let
buttonRightSide
=
stackButton
.
frame
.
origin
.
x
+
stackButton
.
bounds
.
width
let
buttonCenterX
=
(
buttonRightSide
+
stackButton
.
frame
.
origin
.
x
)
/
2
let
levelsCount
=
CGFloat
(
Set
(
speeds
)
.
count
)
let
levelHeight
=
(
graphView
.
frame
.
height
/
CGFloat
(
levelsCount
))
.
rounded
(
.
down
)
let
graphFrame
=
CGRect
(
x
:
0
,
y
:
0
,
width
:
graphView
.
frame
.
width
,
height
:
graphView
.
frame
.
height
-
5
)
var
pointLevel
=
graphFrame
.
origin
.
y
+
((
maxWindSpeed
-
speeds
[
index
])
*
levelHeight
)
pointLevel
=
max
(
pointLevel
,
graphFrame
.
origin
.
y
)
pointLevel
=
min
(
graphFrame
.
height
,
pointLevel
)
points
.
append
(
.
init
(
x
:
buttonCenterX
,
y
:
pointLevel
))
}
hourlyGraphPoints
=
HourlyGraphPoints
(
points
:
points
)
}
private
func
drawGraph
()
{
guard
let
periodButtons
=
stackView
.
arrangedSubviews
as?
[
PeriodButtonProtocol
],
...
...
@@ -226,7 +259,7 @@ class ForecastTimePeriodView: UIView {
return
}
switch
current
TimePeriod
{
switch
current
ForecastType
{
case
.
daily
:
self
.
graphView
.
drawMainGraph
(
with
:
dailyGraphPoints
.
maxTempPoints
)
self
.
graphView
.
drawAdditionalGraph
(
with
:
dailyGraphPoints
.
minTempPoints
)
...
...
@@ -235,6 +268,10 @@ class ForecastTimePeriodView: UIView {
self
.
graphView
.
drawMainGraph
(
with
:
hourlyGraphPoints
.
points
)
self
.
graphView
.
drawAdditionalGraph
(
with
:
[
CGPoint
]())
self
.
tintGraphAt
(
button
:
selectedButton
)
case
.
wind
:
self
.
graphView
.
drawWindGraph
(
with
:
hourlyGraphPoints
.
points
)
self
.
graphView
.
drawAdditionalGraph
(
with
:
[
CGPoint
]())
self
.
tintGraphAt
(
button
:
selectedButton
)
}
print
(
"[ForecastTimePeriod] Draw graph"
)
...
...
@@ -243,13 +280,13 @@ class ForecastTimePeriodView: UIView {
private
func
tintGraphAt
(
button
:
PeriodButtonProtocol
)
{
guard
button
.
tinted
else
{
return
}
switch
current
TimePeriod
{
switch
current
ForecastType
{
case
.
daily
:
self
.
graphView
.
tintGraphFrom
(
startPointX
:
button
.
frame
.
origin
.
x
,
endPointX
:
button
.
frame
.
origin
.
x
+
button
.
bounds
.
width
)
self
.
graphView
.
tintMainDotAt
(
point
:
dailyGraphPoints
.
maxTempPoints
[
button
.
index
])
self
.
graphView
.
tintAdditionalDotAt
(
point
:
dailyGraphPoints
.
minTempPoints
[
button
.
index
])
case
.
hourly
:
case
.
hourly
,
.
wind
:
self
.
graphView
.
tintGraphFrom
(
startPointX
:
button
.
frame
.
origin
.
x
,
endPointX
:
button
.
frame
.
origin
.
x
+
button
.
bounds
.
width
)
self
.
graphView
.
tintMainDotAt
(
point
:
hourlyGraphPoints
.
points
[
button
.
index
])
...
...
1Weather/UI/Helpers/ForecastTimePeriod/ForecastWindButton.swift
0 → 100644
View file @
bb0e57d7
//
// ForecastWindButton.swift
// 1Weather
//
// Created by Dmitry Stepanets on 23.03.2021.
//
import
UIKit
class
ForecastWindButton
:
UIControl
,
PeriodButtonProtocol
{
//Private
private
static
var
hourlyFormatter
:
DateFormatter
=
{
let
fmt
=
DateFormatter
()
fmt
.
dateFormat
=
"h a"
return
fmt
}()
private
let
kGraphInset
:
CGFloat
=
10
private
let
windInfoLabel
=
UILabel
()
private
let
directionImageView
=
UIImageView
()
private
let
indicatorImageView
=
UIImageView
()
private
let
timeLabel
=
UILabel
()
//Public
var
index
:
Int
=
-
1
var
tinted
:
Bool
{
return
true
}
var
graphRect
:
CGRect
{
let
topInset
=
self
.
directionImageView
.
frame
.
origin
.
y
+
self
.
directionImageView
.
frame
.
size
.
height
+
kGraphInset
return
.
init
(
x
:
0
,
y
:
topInset
,
width
:
self
.
bounds
.
width
,
height
:
self
.
indicatorImageView
.
frame
.
origin
.
y
-
topInset
-
kGraphInset
)
}
required
init
()
{
super
.
init
(
frame
:
.
zero
)
prepareButton
()
prepareInfoLabel
()
prepareDirectionImageView
()
prepareTimeLabel
()
prepareWindIndicator
()
}
required
init
?(
coder
:
NSCoder
)
{
fatalError
(
"init(coder:) has not been implemented"
)
}
override
var
isSelected
:
Bool
{
didSet
{
if
isSelected
{
self
.
backgroundColor
=
UIColor
.
white
self
.
windInfoLabel
.
font
=
AppFont
.
SFPro
.
bold
(
size
:
16
)
self
.
timeLabel
.
font
=
AppFont
.
SFPro
.
bold
(
size
:
12
)
self
.
layer
.
shadowColor
=
UIColor
(
hex
:
0xe5e6f4
)
.
cgColor
self
.
layer
.
shadowOffset
=
.
init
(
width
:
5
,
height
:
5
)
self
.
layer
.
shadowRadius
=
18
self
.
layer
.
shadowOpacity
=
0.64
}
else
{
self
.
backgroundColor
=
UIColor
.
white
.
withAlphaComponent
(
0.5
)
self
.
windInfoLabel
.
font
=
AppFont
.
SFPro
.
regular
(
size
:
16
)
self
.
timeLabel
.
font
=
AppFont
.
SFPro
.
regular
(
size
:
12
)
self
.
layer
.
shadowColor
=
UIColor
.
clear
.
cgColor
self
.
layer
.
shadowOffset
=
.
zero
self
.
layer
.
shadowRadius
=
0
self
.
layer
.
shadowOpacity
=
0
}
}
}
func
configure
(
dailyWeather
:
DailyWeather
)
{
assertionFailure
(
"Not implemented"
)
}
func
configure
(
hourlyWeather
:
HourlyWeather
)
{
let
direction
=
hourlyWeather
.
windDirection
?
.
rawValue
??
"--"
let
speed
=
hourlyWeather
.
windSpeed
?
.
string
??
"--"
windInfoLabel
.
text
=
"
\(
direction
.
uppercased
()
)\n\(
speed
.
uppercased
()
)
"
let
degrees
=
hourlyWeather
.
windDirection
?
.
degrees
??
0
directionImageView
.
transform
=
CGAffineTransform
(
rotationAngle
:
degrees
*
.
pi
/
180
)
if
Calendar
.
isNow
(
fromDate
:
hourlyWeather
.
date
,
timeZone
:
hourlyWeather
.
timeZone
)
{
timeLabel
.
text
=
"day.now"
.
localized
()
.
uppercased
()
}
else
{
ForecastWindButton
.
hourlyFormatter
.
timeZone
=
hourlyWeather
.
timeZone
timeLabel
.
text
=
ForecastWindButton
.
hourlyFormatter
.
string
(
from
:
hourlyWeather
.
date
)
}
}
}
//MARK:- Prepare
private
extension
ForecastWindButton
{
func
prepareButton
()
{
clipsToBounds
=
false
backgroundColor
=
UIColor
.
white
layer
.
cornerRadius
=
12
layer
.
borderColor
=
UIColor
(
hex
:
0xeceef6
)
.
cgColor
layer
.
borderWidth
=
1
/
UIScreen
.
main
.
scale
self
.
snp
.
makeConstraints
{
(
make
)
in
make
.
width
.
equalTo
(
70
)
}
}
func
prepareInfoLabel
()
{
windInfoLabel
.
isUserInteractionEnabled
=
false
windInfoLabel
.
font
=
AppFont
.
SFPro
.
regular
(
size
:
16
)
windInfoLabel
.
textColor
=
ThemeManager
.
currentTheme
.
secondaryTextColor
windInfoLabel
.
textAlignment
=
.
center
windInfoLabel
.
numberOfLines
=
2
windInfoLabel
.
lineBreakMode
=
.
byWordWrapping
addSubview
(
windInfoLabel
)
windInfoLabel
.
snp
.
makeConstraints
{
(
make
)
in
make
.
top
.
equalToSuperview
()
.
inset
(
16
)
make
.
left
.
right
.
equalToSuperview
()
.
inset
(
2
)
}
}
func
prepareDirectionImageView
()
{
directionImageView
.
contentMode
=
.
scaleAspectFit
directionImageView
.
image
=
UIImage
(
named
:
"wind_direction"
)
directionImageView
.
tintColor
=
UIColor
(
hex
:
0x8fc2ff
)
addSubview
(
directionImageView
)
directionImageView
.
snp
.
makeConstraints
{
(
make
)
in
make
.
top
.
equalTo
(
windInfoLabel
.
snp
.
bottom
)
.
offset
(
36
)
make
.
width
.
height
.
equalTo
(
18
)
make
.
centerX
.
equalToSuperview
()
}
}
func
prepareWindIndicator
()
{
indicatorImageView
.
isUserInteractionEnabled
=
false
indicatorImageView
.
isHidden
=
true
indicatorImageView
.
contentMode
=
.
scaleAspectFit
indicatorImageView
.
image
=
UIImage
(
named
:
"blowingDust"
)
addSubview
(
indicatorImageView
)
indicatorImageView
.
snp
.
makeConstraints
{
(
make
)
in
make
.
width
.
height
.
equalTo
(
12
)
make
.
centerX
.
equalToSuperview
()
make
.
bottom
.
equalTo
(
timeLabel
.
snp
.
top
)
.
offset
(
-
15
)
}
}
func
prepareTimeLabel
()
{
timeLabel
.
isUserInteractionEnabled
=
false
timeLabel
.
font
=
AppFont
.
SFPro
.
regular
(
size
:
12
)
timeLabel
.
textColor
=
ThemeManager
.
currentTheme
.
secondaryTextColor
timeLabel
.
text
=
"9 AM"
addSubview
(
timeLabel
)
timeLabel
.
snp
.
makeConstraints
{
(
make
)
in
make
.
centerX
.
equalToSuperview
()
make
.
bottom
.
equalToSuperview
()
.
inset
(
20
)
}
}
}
1Weather/UI/Helpers/ForecastTimePeriod/GraphLine.swift
View file @
bb0e57d7
...
...
@@ -15,6 +15,7 @@ struct LineDot {
struct
GraphLine
{
//Private
private
let
kInset
:
CGFloat
=
10
private
let
kIntersectAccuracy
:
CGFloat
=
0
private
var
points
=
[
CGPoint
]()
private
let
settings
:
GraphLineSettings
...
...
@@ -107,7 +108,7 @@ struct GraphLine {
p1
:
.
init
(
x
:
startPointX
+
kIntersectAccuracy
,
y
:
self
.
onGetGraphRect
()
.
height
))
let
rightLine
=
LineSegment
(
p0
:
.
init
(
x
:
endPointX
-
kIntersectAccuracy
,
y
:
0
),
p1
:
.
init
(
x
:
endPointX
-
kIntersectAccuracy
,
y
:
self
.
onGetGraphRect
()
.
height
))
//Get all sections for the given tint and cut them
let
tintPath
=
UIBezierPath
()
for
section
in
sections
{
...
...
@@ -115,10 +116,10 @@ struct GraphLine {
let
maxLeftPointX
=
max
(
section
.
startingPoint
.
x
,
leftLine
.
startingPoint
.
x
)
let
minRightPointX
=
min
(
section
.
endingPoint
.
x
,
rightLine
.
endingPoint
.
x
)
let
leftBoundary
=
LineSegment
(
p0
:
.
init
(
x
:
maxLeftPointX
,
y
:
0
),
p1
:
.
init
(
x
:
maxLeftPointX
,
y
:
self
.
onGetGraphRect
()
.
height
))
let
rightBoundary
=
LineSegment
(
p0
:
.
init
(
x
:
minRightPointX
,
y
:
0
),
p1
:
.
init
(
x
:
minRightPointX
,
y
:
self
.
onGetGraphRect
()
.
height
))
let
leftBoundary
=
LineSegment
(
p0
:
.
init
(
x
:
maxLeftPointX
,
y
:
-
kInset
),
p1
:
.
init
(
x
:
maxLeftPointX
,
y
:
self
.
onGetGraphRect
()
.
height
+
kInset
*
2
))
let
rightBoundary
=
LineSegment
(
p0
:
.
init
(
x
:
minRightPointX
,
y
:
-
kInset
),
p1
:
.
init
(
x
:
minRightPointX
,
y
:
self
.
onGetGraphRect
()
.
height
+
kInset
*
2
))
if
let
subcurve
=
getSubcurvePath
(
baseCurve
:
section
,
leftBoundary
:
leftBoundary
,
rightBoundary
:
rightBoundary
)
...
...
@@ -136,7 +137,7 @@ struct GraphLine {
func
tintDotAt
(
point
:
CGPoint
)
{
lineDots
.
forEach
{
$0
.
shape
.
strokeColor
=
settings
.
c
olor
.
cgColor
$0
.
shape
.
strokeColor
=
settings
.
dotC
olor
.
cgColor
}
guard
let
dotToTint
=
(
lineDots
.
first
{
$0
.
center
==
point
})
else
{
return
}
...
...
@@ -167,7 +168,7 @@ struct GraphLine {
endAngle
:
.
pi
*
2
,
clockwise
:
true
)
.
cgPath
dotShape
.
fillColor
=
UIColor
.
white
.
cgColor
dotShape
.
strokeColor
=
settings
.
c
olor
.
cgColor
dotShape
.
strokeColor
=
settings
.
dotC
olor
.
cgColor
dotShape
.
lineWidth
=
settings
.
dotLineWidth
lineDots
.
append
(
.
init
(
center
:
$0
,
shape
:
dotShape
))
}
...
...
1Weather/UI/Helpers/ForecastTimePeriod/GraphLineSettings.swift
View file @
bb0e57d7
...
...
@@ -17,5 +17,6 @@ struct GraphLineSettings {
//General
let
color
:
UIColor
let
dotColor
:
UIColor
let
tintColor
:
UIColor
}
1Weather/UI/Helpers/ForecastTimePeriod/GraphView.swift
View file @
bb0e57d7
...
...
@@ -14,12 +14,20 @@ class GraphView: UIView {
dotRadius
:
2.5
,
dotLineWidth
:
2
,
color
:
ThemeManager
.
currentTheme
.
graphColor
,
dotColor
:
ThemeManager
.
currentTheme
.
graphColor
,
tintColor
:
ThemeManager
.
currentTheme
.
graphTintColor
)
private
let
additionalLineSettings
=
GraphLineSettings
(
lineWidth
:
2
,
dotRadius
:
2
,
dotLineWidth
:
1
,
color
:
UIColor
(
hex
:
0xa4a4a4
)
.
withAlphaComponent
(
0.7
),
dotColor
:
UIColor
(
hex
:
0xa4a4a4
)
.
withAlphaComponent
(
0.7
),
tintColor
:
UIColor
(
hex
:
0x434343
)
.
withAlphaComponent
(
0.7
))
private
let
windLineSettings
=
GraphLineSettings
(
lineWidth
:
3
,
dotRadius
:
2.5
,
dotLineWidth
:
2
,
color
:
UIColor
(
hex
:
0x8fc1ff
),
dotColor
:
UIColor
(
hex
:
0x2292fa
),
tintColor
:
UIColor
(
hex
:
0x2292fa
))
private
lazy
var
mainLine
:
GraphLine
=
{
let
line
=
GraphLine
(
settings
:
mainLineSettings
,
onGetGraphRect
:
{
...
...
@@ -35,6 +43,13 @@ class GraphView: UIView {
return
line
}()
private
lazy
var
windLine
:
GraphLine
=
{
let
line
=
GraphLine
(
settings
:
windLineSettings
,
onGetGraphRect
:
{
return
self
.
frame
})
return
line
}()
//MARK:- View life cycle
init
()
{
...
...
@@ -47,6 +62,9 @@ class GraphView: UIView {
layer
.
insertSublayer
(
additionalLine
.
lineShape
,
at
:
0
)
layer
.
insertSublayer
(
additionalLine
.
tintLineShape
,
at
:
0
)
layer
.
insertSublayer
(
windLine
.
lineShape
,
at
:
0
)
layer
.
insertSublayer
(
windLine
.
tintLineShape
,
at
:
1
)
}
required
init
?(
coder
:
NSCoder
)
{
...
...
@@ -68,13 +86,22 @@ class GraphView: UIView {
}
}
public
func
drawWindGraph
(
with
points
:[
CGPoint
])
{
windLine
.
updateWith
(
points
:
points
)
windLine
.
lineDots
.
forEach
{
layer
.
addSublayer
(
$0
.
shape
)
}
}
public
func
tintGraphFrom
(
startPointX
:
CGFloat
,
endPointX
:
CGFloat
)
{
mainLine
.
tintLineFrom
(
startPointX
:
startPointX
,
to
:
endPointX
)
additionalLine
.
tintLineFrom
(
startPointX
:
startPointX
,
to
:
endPointX
)
windLine
.
tintLineFrom
(
startPointX
:
startPointX
,
to
:
endPointX
)
}
public
func
tintMainDotAt
(
point
:
CGPoint
)
{
mainLine
.
tintDotAt
(
point
:
point
)
windLine
.
tintDotAt
(
point
:
point
)
}
public
func
tintAdditionalDotAt
(
point
:
CGPoint
)
{
...
...
1Weather/UI/View controllers/Forecast/Cells/ForecastCellFactory.swift
View file @
bb0e57d7
...
...
@@ -22,7 +22,7 @@ private enum HourlyForecastCellType: Int, CaseIterable {
case
day
case
tempInfo
case
precipitation
//
case wind
case
wind
}
class
ForecastCellFactory
{
...
...
@@ -49,6 +49,7 @@ class ForecastCellFactory {
registerCell
(
type
:
ForecastHourlyCell
.
self
,
tableView
:
tableView
)
registerCell
(
type
:
ForecastInfoCell
.
self
,
tableView
:
tableView
)
registerCell
(
type
:
PrecipitationCell
.
self
,
tableView
:
tableView
)
registerCell
(
type
:
ForecastWindSpeedCell
.
self
,
tableView
:
tableView
)
registerCell
(
type
:
CitySunCell
.
self
,
tableView
:
tableView
)
registerCell
(
type
:
CityMoonCell
.
self
,
tableView
:
tableView
)
}
...
...
@@ -136,6 +137,12 @@ class ForecastCellFactory {
cell
.
configure
(
with
:
hourly
)
}
return
cell
case
.
wind
:
let
cell
=
dequeueReusableCell
(
type
:
ForecastWindSpeedCell
.
self
,
tableView
:
tableView
,
indexPath
:
indexPath
)
if
let
hourly
=
forecastViewModel
.
location
?
.
hourly
{
cell
.
configure
(
with
:
hourly
)
}
return
cell
}
}
...
...
1Weather/UI/View controllers/Forecast/Cells/ForecastDailyCell.swift
View file @
bb0e57d7
...
...
@@ -38,7 +38,7 @@ class ForecastDailyCell: UITableViewCell {
public
func
configure
(
daily
:[
DailyWeather
],
offset
:
CGFloat
=
0
,
selectedButtonIndex
:
Int
=
0
)
{
self
.
forecastTimePeriodView
.
set
(
daily
:
daily
,
hourly
:
nil
)
if
self
.
forecastTimePeriodView
.
isEmpty
{
self
.
forecastTimePeriodView
.
set
(
timePeriod
:
.
daily
,
buttonType
:
ForecastDetailPeriodButton
.
self
)
self
.
forecastTimePeriodView
.
set
(
forecastType
:
.
daily
,
buttonType
:
ForecastDetailPeriodButton
.
self
)
}
self
.
forecastTimePeriodView
.
selectButtonAt
(
index
:
selectedButtonIndex
)
self
.
forecastTimePeriodView
.
update
(
offset
:
offset
)
...
...
1Weather/UI/View controllers/Forecast/Cells/ForecastHourlyCell.swift
View file @
bb0e57d7
...
...
@@ -31,7 +31,7 @@ class ForecastHourlyCell: UITableViewCell {
public
func
configure
(
hourly
:[
HourlyWeather
])
{
self
.
forecastTimePeriodView
.
set
(
daily
:
nil
,
hourly
:
hourly
)
if
self
.
forecastTimePeriodView
.
isEmpty
{
self
.
forecastTimePeriodView
.
set
(
timePeriod
:
.
hourly
,
buttonType
:
ForecastPeriodButton
.
self
)
self
.
forecastTimePeriodView
.
set
(
forecastType
:
.
hourly
,
buttonType
:
ForecastPeriodButton
.
self
)
}
}
}
...
...
1Weather/UI/View controllers/Forecast/Cells/ForecastWindSpeedCell.swift
0 → 100644
View file @
bb0e57d7
//
// ForecastWindSpeedCell.swift
// 1Weather
//
// Created by Dmitry Stepanets on 23.03.2021.
//
import
UIKit
class
ForecastWindSpeedCell
:
UITableViewCell
{
//Private
private
let
headingLabel
=
UILabel
()
private
let
timePeriodView
=
ForecastTimePeriodView
()
private
let
summaryView
=
UIView
()
private
let
summaryImageView
=
UIImageView
()
private
let
summaryLabel
=
UILabel
()
override
init
(
style
:
UITableViewCell
.
CellStyle
,
reuseIdentifier
:
String
?)
{
super
.
init
(
style
:
style
,
reuseIdentifier
:
reuseIdentifier
)
prepareCell
()
prepareHeadingLabel
()
prepareTimePeriodView
()
prepareSummaryView
()
}
required
init
?(
coder
:
NSCoder
)
{
fatalError
(
"init(coder:) has not been implemented"
)
}
public
func
configure
(
with
hourly
:[
HourlyWeather
])
{
self
.
timePeriodView
.
set
(
daily
:
nil
,
hourly
:
hourly
)
if
self
.
timePeriodView
.
isEmpty
{
self
.
timePeriodView
.
set
(
forecastType
:
.
wind
,
buttonType
:
ForecastWindButton
.
self
)
}
}
}
//MARK:- Prepare
private
extension
ForecastWindSpeedCell
{
func
prepareCell
()
{
selectionStyle
=
.
none
contentView
.
backgroundColor
=
ThemeManager
.
currentTheme
.
baseBackgroundColor
}
func
prepareHeadingLabel
()
{
headingLabel
.
font
=
AppFont
.
SFPro
.
bold
(
size
:
18
)
headingLabel
.
textColor
=
ThemeManager
.
currentTheme
.
primaryTextColor
headingLabel
.
text
=
"condition.wind"
.
localized
()
contentView
.
addSubview
(
headingLabel
)
headingLabel
.
snp
.
makeConstraints
{
(
make
)
in
make
.
left
.
top
.
equalToSuperview
()
.
inset
(
18
)
}
}
func
prepareTimePeriodView
()
{
contentView
.
addSubview
(
timePeriodView
)
timePeriodView
.
snp
.
makeConstraints
{
(
make
)
in
make
.
left
.
equalToSuperview
()
make
.
right
.
equalToSuperview
()
make
.
top
.
equalTo
(
headingLabel
.
snp
.
bottom
)
.
offset
(
18
)
.
priority
(
.
medium
)
make
.
height
.
equalTo
(
267
)
}
}
func
prepareSummaryView
()
{
summaryImageView
.
contentMode
=
.
scaleAspectFit
summaryImageView
.
image
=
UIImage
(
named
:
"blowingDust"
)
summaryView
.
addSubview
(
summaryImageView
)
summaryImageView
.
snp
.
makeConstraints
{
(
make
)
in
make
.
left
.
equalToSuperview
()
.
inset
(
20
)
make
.
centerY
.
equalToSuperview
()
make
.
width
.
height
.
equalTo
(
12
)
}
summaryLabel
.
font
=
AppFont
.
SFPro
.
regular
(
size
:
13
)
summaryLabel
.
textColor
=
ThemeManager
.
currentTheme
.
secondaryTextColor
summaryLabel
.
text
=
"Strongest winds between 12 AM - 1 PM"
summaryView
.
addSubview
(
summaryLabel
)
summaryLabel
.
snp
.
makeConstraints
{
(
make
)
in
make
.
left
.
equalTo
(
summaryImageView
.
snp
.
right
)
.
offset
(
8
)
make
.
right
.
equalToSuperview
()
.
inset
(
8
)
make
.
centerY
.
equalToSuperview
()
}
summaryView
.
backgroundColor
=
UIColor
(
hex
:
0xd9ebfe
)
.
withAlphaComponent
(
0.5
)
summaryView
.
layer
.
cornerRadius
=
12
contentView
.
addSubview
(
summaryView
)
summaryView
.
snp
.
makeConstraints
{
(
make
)
in
make
.
left
.
right
.
equalToSuperview
()
.
inset
(
18
)
make
.
height
.
equalTo
(
40
)
make
.
top
.
equalTo
(
timePeriodView
.
snp
.
bottom
)
.
offset
(
20
)
make
.
bottom
.
equalToSuperview
()
.
inset
(
15
)
}
}
}
1Weather/UI/View controllers/Today/Cells/CityForecastTimePeriodCell.swift
View file @
bb0e57d7
...
...
@@ -46,7 +46,8 @@ class CityForecastTimePeriodCell: UITableViewCell {
return
}
self
.
forecastTimePeriodView
.
set
(
timePeriod
:
timePeriod
,
buttonType
:
ForecastPeriodButton
.
self
)
let
forecastType
=
timePeriod
==
.
daily
?
ForecastType
.
daily
:
ForecastType
.
hourly
self
.
forecastTimePeriodView
.
set
(
forecastType
:
forecastType
,
buttonType
:
ForecastPeriodButton
.
self
)
}
}
...
...
1Weather/UI/View controllers/Today/Cells/CityPrecipCell/PrecipButton.swift
View file @
bb0e57d7
...
...
@@ -89,17 +89,13 @@ class PrecipButton: UIControl {
self
.
precipView
.
set
(
value
:
CGFloat
(
percent
)
/
100.0
)
self
.
valueLabel
.
text
=
"
\(
Int
(
percent
)
)
%"
if
let
nowDate
=
Date
.
nowDate
(
timeZone
:
hourly
.
timeZone
)
{
if
Calendar
.
timeZoneCalendar
(
timeZone
:
hourly
.
timeZone
)
.
isDate
(
hourly
.
date
,
equalTo
:
nowDate
,
toGranularity
:
.
hour
)
{
self
.
timeLabel
.
text
=
"day.now"
.
localized
()
.
uppercased
()
}
else
{
PrecipButton
.
hourlyFormatter
.
timeZone
=
hourly
.
timeZone
self
.
timeLabel
.
text
=
PrecipButton
.
hourlyFormatter
.
string
(
from
:
hourly
.
date
)
}
if
Calendar
.
isNow
(
fromDate
:
hourly
.
date
,
timeZone
:
hourly
.
timeZone
)
{
self
.
timeLabel
.
text
=
"day.now"
.
localized
()
.
uppercased
()
}
else
{
self
.
timeLabel
.
text
=
"--"
PrecipButton
.
hourlyFormatter
.
timeZone
=
hourly
.
timeZone
self
.
timeLabel
.
text
=
PrecipButton
.
hourlyFormatter
.
string
(
from
:
hourly
.
date
)
}
}
}
...
...
Pods/Pods.xcodeproj/xcuserdata/dstepanets.xcuserdatad/xcschemes/xcschememanagement.plist
View file @
bb0e57d7
...
...
@@ -52,7 +52,7 @@
<
k
e
y
>
XMLCoder.xcscheme_
^#
shared
#^
_
<
/k
e
y
>
<
d
i
c
t
>
<
k
e
y
>
orderHint
<
/k
e
y
>
<
int
e
g
e
r
>
5
<
/int
e
g
e
r
>
<
int
e
g
e
r
>
6
<
/int
e
g
e
r
>
<
/
d
i
c
t
>
<
/
d
i
c
t
>
<
k
e
y
>
SuppressBuildableAutocreation
<
/k
e
y
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment