Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
M
Material
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
Material
Commits
b4e1d8ef
Commit
b4e1d8ef
authored
Oct 12, 2018
by
Orkhan Alikhanov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added DialogView
parent
77a779df
Show whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
430 additions
and
0 deletions
+430
-0
Sources/iOS/DialogView.swift
+430
-0
No files found.
Sources/iOS/DialogView.swift
0 → 100644
View file @
b4e1d8ef
/*
* Copyright (C) 2018, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* Original Inspiration & Author
* Copyright (C) 2018 Orkhan Alikhanov <orkhan.alikhanov@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of CosmicMind nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import
UIKit
private
class
DialogScrollView
:
UIScrollView
{
/// A weak reference to DialogView.
weak
var
dialogView
:
DialogView
?
override
func
layoutSubviews
()
{
super
.
layoutSubviews
()
dialogView
?
.
layoutDividers
()
}
}
open
class
DialogView
:
View
,
Themeable
{
/// A container view for title area.
public
let
titleArea
=
UIView
()
/// A container view for button area.
public
let
buttonArea
=
UIView
()
/// A container view for content area.
public
let
contentArea
=
UIView
()
/// A scroll view containing contentArea.
public
let
scrollView
:
UIScrollView
=
DialogScrollView
()
/// A UILabel.
public
let
titleLabel
=
UILabel
()
/// A UILabel.
public
let
detailsLabel
=
UILabel
()
/// A Button.
public
let
neutralButton
=
FlatButton
()
/// A Button.
public
let
positiveButton
=
FlatButton
()
/// A Button.
public
let
negativeButton
=
FlatButton
()
/// Maximum size of the dialog.
open
var
maxSize
=
CGSize
(
width
:
200
,
height
:
300
)
{
didSet
{
invalidateIntrinsicContentSize
()
}
}
open
override
func
prepare
()
{
super
.
prepare
()
depthPreset
=
.
depth5
cornerRadiusPreset
=
.
cornerRadius2
prepareTitleArea
()
prepareTitleLabel
()
prepareScrollView
()
prepareContentArea
()
prepareDetailsLabel
()
prepareButtonArea
()
prepareButtons
()
applyCurrentTheme
()
}
open
override
var
intrinsicContentSize
:
CGSize
{
return
sizeThatFits
(
maxSize
)
}
open
override
func
sizeThatFits
(
_
size
:
CGSize
)
->
CGSize
{
var
w
:
CGFloat
=
0
func
setW
(
_
newW
:
CGFloat
)
{
w
=
max
(
w
,
newW
)
w
=
min
(
w
,
size
.
width
)
}
setW
(
titleAreaSizeThatFits
(
width
:
size
.
width
)
.
width
)
setW
(
buttonAreaSizeThatFits
(
width
:
size
.
width
)
.
width
)
setW
(
contentAreaSizeThatFits
(
width
:
size
.
width
)
.
width
)
var
h
:
CGFloat
=
0
h
+=
titleAreaSizeThatFits
(
width
:
w
)
.
height
h
+=
buttonAreaSizeThatFits
(
width
:
w
)
.
height
h
+=
contentAreaSizeThatFits
(
width
:
w
)
.
height
h
=
min
(
h
,
size
.
height
)
return
CGSize
(
width
:
w
,
height
:
h
)
}
open
override
func
layoutSubviews
()
{
super
.
layoutSubviews
()
layoutTitleArea
()
layoutButtonArea
()
layoutContentArea
()
layoutScrollView
()
layoutDividers
()
/// Position button area after having correct sizes.
buttonArea
.
frame
.
origin
.
y
=
scrollView
.
frame
.
maxY
}
/**
Calculates the size for title area that best fits the specified width.
- Parameter width: A CGFloat.
- Returns: Calculated CGSize.
*/
open
func
titleAreaSizeThatFits
(
width
:
CGFloat
)
->
CGSize
{
guard
!
titleLabel
.
isEmpty
else
{
return
.
zero
}
let
insets
=
Constants
.
titleArea
.
insets
var
size
=
titleLabel
.
sizeThatFits
(
CGSize
(
width
:
width
-
insets
.
left
-
insets
.
right
,
height
:
.
greatestFiniteMagnitude
))
size
.
width
+=
insets
.
left
+
insets
.
right
size
.
height
+=
insets
.
top
+
insets
.
bottom
return
size
}
/**
Calculates the size for button area that best fits the specified width.
- Parameter width: A CGFloat.
- Returns: Calculated CGSize.
*/
open
func
buttonAreaSizeThatFits
(
width
:
CGFloat
)
->
CGSize
{
guard
!
nonHiddenButtons
.
isEmpty
else
{
return
.
zero
}
let
isStacked
=
requiredButtonAreaWidth
>
width
let
buttonsHeight
=
Constants
.
button
.
height
*
CGFloat
(
isStacked
?
nonHiddenButtons
.
count
:
1
)
let
buttonsInterim
=
isStacked
?
CGFloat
(
nonHiddenButtons
.
count
-
1
)
*
Constants
.
button
.
interimStacked
:
0
let
insets
=
isStacked
?
Constants
.
buttonArea
.
insetsStacked
:
Constants
.
buttonArea
.
insets
let
h
=
buttonsInterim
+
buttonsHeight
+
insets
.
bottom
+
insets
.
top
let
w
=
min
(
width
,
isStacked
?
requiredButtonAreaWidthForStacked
:
requiredButtonAreaWidth
)
return
CGSize
(
width
:
w
,
height
:
h
)
}
/**
Calculates the size for content area that best fits the specified width.
- Parameter width: A CGFloat.
- Returns: Calculated CGSize.
*/
open
func
contentAreaSizeThatFits
(
width
:
CGFloat
)
->
CGSize
{
guard
!
detailsLabel
.
isEmpty
else
{
return
.
zero
}
let
insets
=
titleLabel
.
isEmpty
?
Constants
.
contentArea
.
insetsNoTitle
:
Constants
.
contentArea
.
insets
var
size
=
detailsLabel
.
sizeThatFits
(
CGSize
(
width
:
width
-
insets
.
left
-
insets
.
right
,
height
:
.
greatestFiniteMagnitude
))
size
.
width
+=
insets
.
left
+
insets
.
right
size
.
height
+=
insets
.
top
+
insets
.
bottom
return
size
}
/**
Applies the given theme.
- Parameter theme: A Theme.
*/
open
func
apply
(
theme
:
Theme
)
{
backgroundColor
=
theme
.
surface
titleLabel
.
textColor
=
theme
.
onSurface
.
withAlphaComponent
(
0.87
)
detailsLabel
.
textColor
=
theme
.
onSurface
.
withAlphaComponent
(
0.60
)
titleArea
.
dividerColor
=
theme
.
onSurface
.
withAlphaComponent
(
0.12
)
buttonArea
.
dividerColor
=
theme
.
onSurface
.
withAlphaComponent
(
0.12
)
}
}
private
extension
DialogView
{
/// Prepares titleArea.
func
prepareTitleArea
()
{
addSubview
(
titleArea
)
titleArea
.
dividerColor
=
Color
.
darkText
.
dividers
titleArea
.
dividerThickness
=
1
titleArea
.
dividerAlignment
=
.
bottom
}
/// Prepares titleTitle.
func
prepareTitleLabel
()
{
titleArea
.
addSubview
(
titleLabel
)
titleLabel
.
font
=
RobotoFont
.
bold
(
with
:
20
)
titleLabel
.
textColor
=
Color
.
darkText
.
primary
titleLabel
.
numberOfLines
=
0
}
/// Prepares buttonArea.
func
prepareButtonArea
()
{
addSubview
(
buttonArea
)
buttonArea
.
dividerColor
=
Color
.
darkText
.
dividers
buttonArea
.
dividerThickness
=
1
buttonArea
.
dividerAlignment
=
.
top
}
/// Prepares buttons.
func
prepareButtons
()
{
[
positiveButton
,
negativeButton
,
neutralButton
]
.
forEach
{
buttonArea
.
addSubview
(
$0
)
$0
.
titleLabel
?
.
font
=
RobotoFont
.
medium
(
with
:
14
)
$0
.
contentEdgeInsets
=
Constants
.
button
.
insets
$0
.
cornerRadiusPreset
=
.
cornerRadius1
}
}
/// Prepares scrollView.
func
prepareScrollView
()
{
(
scrollView
as!
DialogScrollView
)
.
dialogView
=
self
addSubview
(
scrollView
)
}
/// Prepares contentArea.
func
prepareContentArea
()
{
scrollView
.
addSubview
(
contentArea
)
}
/// Prepares detailsLabel.
func
prepareDetailsLabel
()
{
contentArea
.
addSubview
(
detailsLabel
)
detailsLabel
.
numberOfLines
=
0
detailsLabel
.
textColor
=
Color
.
darkText
.
secondary
}
}
private
extension
DialogView
{
/// Layout the titleArea.
func
layoutTitleArea
()
{
let
size
=
CGSize
(
width
:
frame
.
width
,
height
:
titleAreaSizeThatFits
(
width
:
frame
.
width
)
.
height
)
titleArea
.
frame
.
size
=
size
guard
!
titleLabel
.
isEmpty
else
{
return
}
let
rect
=
CGRect
(
origin
:
.
zero
,
size
:
size
)
titleLabel
.
frame
=
rect
.
inset
(
by
:
Constants
.
titleArea
.
insets
)
}
/// Layout the buttonArea.
func
layoutButtonArea
()
{
let
width
=
frame
.
width
buttonArea
.
frame
.
size
.
width
=
width
buttonArea
.
frame
.
size
.
height
=
buttonAreaSizeThatFits
(
width
:
width
)
.
height
let
buttons
=
nonHiddenButtons
guard
!
buttons
.
isEmpty
else
{
return
}
let
isStacked
=
requiredButtonAreaWidth
>
width
if
isStacked
{
let
insets
=
Constants
.
buttonArea
.
insetsStacked
buttons
.
forEach
{
let
w
=
min
(
$0
.
optimalWidth
,
width
-
insets
.
left
-
insets
.
right
)
$0
.
frame
.
size
=
CGSize
(
width
:
w
,
height
:
Constants
.
button
.
height
)
$0
.
frame
.
origin
.
x
=
width
-
insets
.
right
-
w
}
positiveButton
.
frame
.
origin
.
y
=
insets
.
top
let
belowPositive
=
positiveButton
.
isHidden
?
insets
.
top
:
positiveButton
.
frame
.
maxY
+
Constants
.
button
.
interimStacked
negativeButton
.
frame
.
origin
.
y
=
belowPositive
neutralButton
.
frame
.
origin
.
y
=
negativeButton
.
isHidden
?
belowPositive
:
negativeButton
.
frame
.
maxY
+
Constants
.
button
.
interimStacked
}
else
{
let
insets
=
Constants
.
buttonArea
.
insets
buttons
.
forEach
{
$0
.
frame
.
size
=
CGSize
(
width
:
$0
.
optimalWidth
,
height
:
Constants
.
button
.
height
)
$0
.
frame
.
origin
.
y
=
insets
.
top
}
neutralButton
.
frame
.
origin
.
x
=
insets
.
left
positiveButton
.
frame
.
origin
.
x
=
width
-
insets
.
right
-
positiveButton
.
frame
.
width
let
maxX
=
positiveButton
.
isHidden
?
width
-
insets
.
right
:
positiveButton
.
frame
.
minX
-
Constants
.
button
.
interim
negativeButton
.
frame
.
origin
.
x
=
maxX
-
negativeButton
.
frame
.
width
}
}
/// Layout the contentArea.
func
layoutContentArea
()
{
let
size
=
CGSize
(
width
:
frame
.
width
,
height
:
contentAreaSizeThatFits
(
width
:
frame
.
width
)
.
height
)
contentArea
.
frame
.
size
=
size
guard
!
detailsLabel
.
isEmpty
else
{
return
}
let
rect
=
CGRect
(
origin
:
.
zero
,
size
:
size
)
let
insets
=
titleArea
.
frame
.
height
==
0
?
Constants
.
contentArea
.
insetsNoTitle
:
Constants
.
contentArea
.
insets
detailsLabel
.
frame
=
rect
.
inset
(
by
:
insets
)
}
/// Layout the scrollView.
func
layoutScrollView
()
{
let
h
=
titleArea
.
frame
.
height
+
buttonArea
.
frame
.
height
let
allowed
=
min
(
frame
.
height
-
h
,
contentArea
.
frame
.
height
)
scrollView
.
frame
.
size
=
CGSize
(
width
:
frame
.
width
,
height
:
max
(
allowed
,
0
))
scrollView
.
frame
.
origin
.
y
=
titleArea
.
frame
.
maxY
scrollView
.
contentSize
=
contentArea
.
frame
.
size
}
/**
Layout the dividers.
This method is also called (by scrollView) when scrolling happens
*/
func
layoutDividers
()
{
let
isScrollable
=
contentArea
.
frame
.
height
>
scrollView
.
frame
.
height
titleArea
.
isDividerHidden
=
titleArea
.
frame
.
height
==
0
||
!
isScrollable
||
scrollView
.
isAtTop
buttonArea
.
isDividerHidden
=
buttonArea
.
frame
.
height
==
0
||
!
isScrollable
||
scrollView
.
isAtBottom
titleArea
.
layoutDivider
()
buttonArea
.
layoutDivider
()
}
}
private
extension
DialogView
{
/// Required width to fit content of buttonArea.
var
requiredButtonAreaWidth
:
CGFloat
{
let
buttons
=
nonHiddenButtons
guard
!
buttons
.
isEmpty
else
{
return
0
}
let
buttonsWidth
:
CGFloat
=
buttons
.
reduce
(
0
)
{
$0
+
$1
.
optimalWidth
}
let
additional
:
CGFloat
=
neutralButton
.
isHidden
?
0
:
8
// additional spacing for neutral button
let
insets
=
Constants
.
buttonArea
.
insets
return
buttonsWidth
+
insets
.
left
+
insets
.
right
+
CGFloat
((
buttons
.
count
-
1
))
*
Constants
.
button
.
interim
+
additional
}
/// Required width to fit statcked content of buttonArea.
var
requiredButtonAreaWidthForStacked
:
CGFloat
{
let
insets
=
Constants
.
buttonArea
.
insetsStacked
return
insets
.
left
+
insets
.
right
+
nonHiddenButtons
.
reduce
(
0
)
{
max
(
$0
,
$1
.
optimalWidth
)
}
}
/// Non-hidden buttons within buttonArea.
var
nonHiddenButtons
:
[
Button
]
{
positiveButton
.
isHidden
=
positiveButton
.
title
(
for
:
.
normal
)?
.
isEmpty
??
true
negativeButton
.
isHidden
=
negativeButton
.
title
(
for
:
.
normal
)?
.
isEmpty
??
true
neutralButton
.
isHidden
=
neutralButton
.
title
(
for
:
.
normal
)?
.
isEmpty
??
true
return
[
positiveButton
,
negativeButton
,
neutralButton
]
.
filter
{
!
$0
.
isHidden
}
}
}
private
extension
UIScrollView
{
/// Checks if scroll view is at the top.
var
isAtTop
:
Bool
{
return
contentOffset
.
y
<=
0
}
/// Checks if scroll view is at the bottom.
var
isAtBottom
:
Bool
{
/// -1 is used to get rid of precision errors
/// make divider appear even when scroll is at the bottom.
return
contentOffset
.
y
>=
(
contentSize
.
height
-
frame
.
height
-
1
)
}
}
private
extension
Button
{
/// Optimal width for dialog button.
var
optimalWidth
:
CGFloat
{
let
size
=
CGSize
(
width
:
.
greatestFiniteMagnitude
,
height
:
Constants
.
button
.
height
)
return
max
(
Constants
.
button
.
minWidth
,
sizeThatFits
(
size
)
.
width
)
}
}
private
extension
UILabel
{
/// Checks if label is empty.
var
isEmpty
:
Bool
{
let
empty
=
text
?
.
isEmpty
??
true
isHidden
=
empty
return
empty
}
}
private
struct
Constants
{
struct
titleArea
{
static
let
insets
=
UIEdgeInsets
(
top
:
24
,
left
:
24
,
bottom
:
20
,
right
:
24
)
}
struct
contentArea
{
static
let
insets
=
UIEdgeInsets
(
top
:
0
,
left
:
24
,
bottom
:
24
,
right
:
24
)
static
let
insetsNoTitle
=
UIEdgeInsets
(
top
:
20
,
left
:
24
,
bottom
:
24
,
right
:
24
)
}
struct
buttonArea
{
static
let
insets
=
UIEdgeInsets
(
top
:
8
,
left
:
8
,
bottom
:
8
,
right
:
8
)
static
let
insetsStacked
=
UIEdgeInsets
(
top
:
6
,
left
:
8
,
bottom
:
14
,
right
:
8
)
}
struct
button
{
static
let
insets
=
UIEdgeInsets
(
top
:
0
,
left
:
8
,
bottom
:
0
,
right
:
8
)
static
let
minWidth
:
CGFloat
=
64
static
let
height
:
CGFloat
=
36
static
let
interimStacked
:
CGFloat
=
12
static
let
interim
:
CGFloat
=
8
}
}
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