Commit 7f344994 by Daniel Dahan Committed by GitHub

Merge pull request #1130 from OrkhanAlikhanov/editor

Addressed multiple issues
parents c2ef0806 dbf37c44
...@@ -173,6 +173,7 @@ ...@@ -173,6 +173,7 @@
9D054A6520D175AC00D0528D /* Material+UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D054A6320D175AC00D0528D /* Material+UIButton.swift */; }; 9D054A6520D175AC00D0528D /* Material+UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D054A6320D175AC00D0528D /* Material+UIButton.swift */; };
9D054A6620D175AC00D0528D /* Material+UILabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D054A6420D175AC00D0528D /* Material+UILabel.swift */; }; 9D054A6620D175AC00D0528D /* Material+UILabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D054A6420D175AC00D0528D /* Material+UILabel.swift */; };
9D39A81B20FE8ED100BA8FA1 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D39A81A20FE8ED100BA8FA1 /* ViewController.swift */; }; 9D39A81B20FE8ED100BA8FA1 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D39A81A20FE8ED100BA8FA1 /* ViewController.swift */; };
9D9089B92118914500605DC9 /* Editor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9089B82118914500605DC9 /* Editor.swift */; };
9DE84D721FF0252600586C8B /* RadioButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D6F1FF0252500586C8B /* RadioButtonGroup.swift */; }; 9DE84D721FF0252600586C8B /* RadioButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D6F1FF0252500586C8B /* RadioButtonGroup.swift */; };
9DE84D731FF0252600586C8B /* BaseButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D701FF0252500586C8B /* BaseButtonGroup.swift */; }; 9DE84D731FF0252600586C8B /* BaseButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D701FF0252500586C8B /* BaseButtonGroup.swift */; };
9DE84D741FF0252600586C8B /* CheckButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D711FF0252500586C8B /* CheckButtonGroup.swift */; }; 9DE84D741FF0252600586C8B /* CheckButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D711FF0252500586C8B /* CheckButtonGroup.swift */; };
...@@ -294,6 +295,7 @@ ...@@ -294,6 +295,7 @@
9D054A6320D175AC00D0528D /* Material+UIButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIButton.swift"; sourceTree = "<group>"; }; 9D054A6320D175AC00D0528D /* Material+UIButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIButton.swift"; sourceTree = "<group>"; };
9D054A6420D175AC00D0528D /* Material+UILabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UILabel.swift"; sourceTree = "<group>"; }; 9D054A6420D175AC00D0528D /* Material+UILabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UILabel.swift"; sourceTree = "<group>"; };
9D39A81A20FE8ED100BA8FA1 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; }; 9D39A81A20FE8ED100BA8FA1 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
9D9089B82118914500605DC9 /* Editor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Editor.swift; sourceTree = "<group>"; };
9DE84D6F1FF0252500586C8B /* RadioButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioButtonGroup.swift; sourceTree = "<group>"; }; 9DE84D6F1FF0252500586C8B /* RadioButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioButtonGroup.swift; sourceTree = "<group>"; };
9DE84D701FF0252500586C8B /* BaseButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseButtonGroup.swift; sourceTree = "<group>"; }; 9DE84D701FF0252500586C8B /* BaseButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseButtonGroup.swift; sourceTree = "<group>"; };
9DE84D711FF0252500586C8B /* CheckButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckButtonGroup.swift; sourceTree = "<group>"; }; 9DE84D711FF0252500586C8B /* CheckButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckButtonGroup.swift; sourceTree = "<group>"; };
...@@ -321,6 +323,7 @@ ...@@ -321,6 +323,7 @@
96BCB79C1CB40DC500C806FE /* TextField.swift */, 96BCB79C1CB40DC500C806FE /* TextField.swift */,
961F18E71CD93E3E008927C5 /* ErrorTextField.swift */, 961F18E71CD93E3E008927C5 /* ErrorTextField.swift */,
9DF58CED20C098C60098968D /* ErrorTextFieldValidator.swift */, 9DF58CED20C098C60098968D /* ErrorTextFieldValidator.swift */,
9D9089B82118914500605DC9 /* Editor.swift */,
); );
name = Text; name = Text;
sourceTree = "<group>"; sourceTree = "<group>";
...@@ -978,6 +981,7 @@ ...@@ -978,6 +981,7 @@
965E811B1DD4D5C800D61E4B /* Switch.swift in Sources */, 965E811B1DD4D5C800D61E4B /* Switch.swift in Sources */,
965E811C1DD4D5C800D61E4B /* TabBar.swift in Sources */, 965E811C1DD4D5C800D61E4B /* TabBar.swift in Sources */,
965E811D1DD4D5C800D61E4B /* TableViewCell.swift in Sources */, 965E811D1DD4D5C800D61E4B /* TableViewCell.swift in Sources */,
9D9089B92118914500605DC9 /* Editor.swift in Sources */,
965E811E1DD4D5C800D61E4B /* TextField.swift in Sources */, 965E811E1DD4D5C800D61E4B /* TextField.swift in Sources */,
965E811F1DD4D5C800D61E4B /* ErrorTextField.swift in Sources */, 965E811F1DD4D5C800D61E4B /* ErrorTextField.swift in Sources */,
965E81211DD4D5C800D61E4B /* TextStorage.swift in Sources */, 965E81211DD4D5C800D61E4B /* TextStorage.swift in Sources */,
......
/*
* Copyright (C) 2015 - 2018, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
* All rights reserved.
*
* 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
public enum EditorPlaceholderAnimation {
case `default`
case hidden
}
open class Editor: View {
public let textView = TextView()
/// A boolean indicating whether the textView is in edit mode.
open var isEditing: Bool {
return textView.isEditing
}
/// A boolean indicating whether the text is empty.
open var isEmpty: Bool {
return textView.isEmpty
}
/// The placeholder UILabel.
@IBInspectable
open var placeholderLabel: UILabel {
return textView.placeholderLabel
}
/// A Boolean that indicates if the placeholder label is animated.
@IBInspectable
open var isPlaceholderAnimated = true
/// Set the placeholder animation value.
open var placeholderAnimation = EditorPlaceholderAnimation.default {
didSet {
updatePlaceholderVisibility()
}
}
/// Placeholder normal text color.
@IBInspectable
open var placeholderNormalColor = Color.darkText.others {
didSet {
updatePlaceholderLabelColor()
}
}
/// Placeholder active text color.
@IBInspectable
open var placeholderActiveColor = Color.blue.base {
didSet {
updatePlaceholderLabelColor()
}
}
/// The scale of the active placeholder in relation to the inactive.
@IBInspectable
open var placeholderActiveScale: CGFloat = 0.75 {
didSet {
layoutPlaceholderLabel()
}
}
/// This property adds a padding to placeholder y position animation
@IBInspectable
open var placeholderVerticalOffset: CGFloat = 0
/// This property adds a padding to placeholder x position animation
@IBInspectable
open var placeholderHorizontalOffset: CGFloat = 0
/// Divider normal height.
@IBInspectable
open var dividerNormalHeight: CGFloat = 1 {
didSet {
updateDividerHeight()
}
}
/// Divider active height.
@IBInspectable
open var dividerActiveHeight: CGFloat = 2 {
didSet {
updateDividerHeight()
}
}
/// Divider normal color.
@IBInspectable
open var dividerNormalColor = Color.grey.lighten2 {
didSet {
updateDividerColor()
}
}
/// Divider active color.
@IBInspectable
open var dividerActiveColor = Color.blue.base {
didSet {
updateDividerColor()
}
}
/// The detailLabel UILabel that is displayed.
@IBInspectable
public let detailLabel = UILabel()
/// The detailLabel text value.
@IBInspectable
open var detail: String? {
get {
return detailLabel.text
}
set(value) {
detailLabel.text = value
layoutSubviews()
}
}
/// The detailLabel text color.
@IBInspectable
open var detailColor = Color.darkText.others {
didSet {
updateDetailLabelColor()
}
}
/// Vertical distance for the detailLabel from the divider.
@IBInspectable
open var detailVerticalOffset: CGFloat = 8 {
didSet {
layoutSubviews()
}
}
/// A reference to titleLabel.textAlignment observation.
private var placeholderLabelTextObserver: NSKeyValueObservation!
/**
A reference to textView.text observation.
Only observes programmatic changes.
*/
private var textViewTextObserver: NSKeyValueObservation!
open override func prepare() {
super.prepare()
prepareDivider()
prepareTextView()
preparePlaceholderLabel()
prepareDetailLabel()
prepareNotificationHandlers()
}
open override func layoutSubviews() {
super.layoutSubviews()
layoutPlaceholderLabel()
layoutDivider()
layoutBottomLabel(label: detailLabel, verticalOffset: detailVerticalOffset)
}
@discardableResult
open override func becomeFirstResponder() -> Bool {
return textView.becomeFirstResponder()
}
@discardableResult
open override func resignFirstResponder() -> Bool {
return textView.resignFirstResponder()
}
open override var inputAccessoryView: UIView? {
get {
return textView.inputAccessoryView
}
set(value) {
textView.inputAccessoryView = value
}
}
}
private extension Editor {
/// Prepares the divider.
func prepareDivider() {
dividerColor = dividerNormalColor
}
/// Prepares the textView.
func prepareTextView() {
layout(textView).edges()
textView.isPlaceholderLabelEnabled = false
textViewTextObserver = textView.observe(\.text) { [weak self] _, _ in
self?.updateEditorState()
}
}
/// Prepares the placeholderLabel.
func preparePlaceholderLabel() {
addSubview(placeholderLabel)
placeholderLabelTextObserver = placeholderLabel.observe(\.text) { [weak self] _, _ in
self?.layoutPlaceholderLabel()
}
}
/// Prepares the detailLabel.
func prepareDetailLabel() {
detailLabel.font = RobotoFont.regular(with: 12)
detailLabel.numberOfLines = 0
detailColor = Color.darkText.others
addSubview(detailLabel)
}
/// Prepares the Notification handlers.
func prepareNotificationHandlers() {
let center = NotificationCenter.default
center.addObserver(self, selector: #selector(handleTextViewTextDidBegin), name: UITextView.textDidBeginEditingNotification, object: textView)
center.addObserver(self, selector: #selector(handleTextViewTextDidChange), name: UITextView.textDidChangeNotification, object: textView)
center.addObserver(self, selector: #selector(handleTextViewTextDidEnd), name: UITextView.textDidEndEditingNotification, object: textView)
}
}
private extension Editor {
/// Updates the placeholderLabel text color.
func updatePlaceholderLabelColor() {
tintColor = placeholderActiveColor
placeholderLabel.textColor = isEditing ? placeholderActiveColor : placeholderNormalColor
}
/// Updates the placeholder visibility.
func updatePlaceholderVisibility() {
guard isEditing else {
placeholderLabel.isHidden = !isEmpty && .hidden == placeholderAnimation
return
}
placeholderLabel.isHidden = .hidden == placeholderAnimation
}
/// Updates the dividerColor.
func updateDividerColor() {
dividerColor = isEditing ? dividerActiveColor : dividerNormalColor
}
/// Updates the dividerThickness.
func updateDividerHeight() {
dividerThickness = isEditing ? dividerActiveHeight : dividerNormalHeight
}
/// Updates the detailLabel text color.
func updateDetailLabelColor() {
detailLabel.textColor = detailColor
}
}
private extension Editor {
/// Layout the placeholderLabel.
func layoutPlaceholderLabel() {
let inset = textView.textContainerInsets
let leftPadding = inset.left + textView.textContainer.lineFragmentPadding
let rightPadding = inset.right + textView.textContainer.lineFragmentPadding
let w = bounds.width - leftPadding - rightPadding
var h = placeholderLabel.sizeThatFits(CGSize(width: w, height: .greatestFiniteMagnitude)).height
h = max(h, textView.minimumTextHeight)
h = min(h, bounds.height - inset.top - inset.bottom)
placeholderLabel.bounds.size = CGSize(width: w, height: h)
guard isEditing || !isEmpty || !isPlaceholderAnimated else {
placeholderLabel.transform = CGAffineTransform.identity
placeholderLabel.frame.origin = CGPoint(x: leftPadding, y: inset.top)
return
}
placeholderLabel.transform = CGAffineTransform(scaleX: placeholderActiveScale, y: placeholderActiveScale)
placeholderLabel.frame.origin.y = -placeholderLabel.frame.height + placeholderVerticalOffset
switch placeholderLabel.textAlignment {
case .left, .natural:
placeholderLabel.frame.origin.x = leftPadding + placeholderHorizontalOffset
case .right:
let scaledWidth = w * placeholderActiveScale
placeholderLabel.frame.origin.x = bounds.width - scaledWidth - rightPadding + placeholderHorizontalOffset
default:break
}
}
/// Layout given label at the bottom with the vertical offset provided.
func layoutBottomLabel(label: UILabel, verticalOffset: CGFloat) {
let c = dividerContentEdgeInsets
label.frame.origin.x = c.left
label.frame.origin.y = bounds.height + verticalOffset
label.frame.size.width = bounds.width - c.left - c.right
label.frame.size.height = label.sizeThatFits(CGSize(width: label.bounds.width, height: .greatestFiniteMagnitude)).height
}
}
private extension Editor {
/// Notification handler for when text editing began.
@objc
func handleTextViewTextDidBegin() {
updateEditorState(animated: true)
}
/// Notification handler for when text changed.
@objc
func handleTextViewTextDidChange() {
updateEditorState()
}
/// Notification handler for when text editing ended.
@objc
func handleTextViewTextDidEnd() {
updateEditorState(animated: true)
}
/// Updates editor.
func updateEditorState(animated: Bool = false) {
updatePlaceholderVisibility()
updatePlaceholderLabelColor()
updateDividerHeight()
updateDividerColor()
guard animated && isPlaceholderAnimated else {
layoutPlaceholderLabel()
return
}
UIView.animate(withDuration: 0.15, animations: layoutPlaceholderLabel)
}
}
public extension Editor {
/// A reference to the textView text.
var text: String! {
get {
return textView.text
}
set(value) {
textView.text = value
}
}
/// A reference to the textView font.
var font: UIFont? {
get {
return textView.font
}
set(value) {
textView.font = value
}
}
/// A reference to the textView placeholder.
var placeholder: String? {
get {
return textView.placeholder
}
set(value) {
textView.placeholder = value
}
}
/// A reference to the textView textAlignment.
var textAlignment: NSTextAlignment {
get {
return textView.textAlignment
}
set(value) {
textView.textAlignment = value
detailLabel.textAlignment = value
}
}
}
...@@ -64,9 +64,14 @@ public protocol TextFieldDelegate: UITextFieldDelegate { ...@@ -64,9 +64,14 @@ public protocol TextFieldDelegate: UITextFieldDelegate {
} }
open class TextField: UITextField { open class TextField: UITextField {
/// Minimum TextField text height.
private let minimumTextHeight: CGFloat = 32
/// Default size when using AutoLayout. /// Default size when using AutoLayout.
open override var intrinsicContentSize: CGSize { open override var intrinsicContentSize: CGSize {
return CGSize(width: bounds.width, height: max(32, super.intrinsicContentSize.height)) let h = textInsets.top + textInsets.bottom + minimumTextHeight
return CGSize(width: bounds.width, height: max(h, super.intrinsicContentSize.height))
} }
/// A Boolean that indicates if the placeholder label is animated. /// A Boolean that indicates if the placeholder label is animated.
...@@ -259,13 +264,9 @@ open class TextField: UITextField { ...@@ -259,13 +264,9 @@ open class TextField: UITextField {
/// Handles the textAlignment of the placeholderLabel. /// Handles the textAlignment of the placeholderLabel.
open override var textAlignment: NSTextAlignment { open override var textAlignment: NSTextAlignment {
get { didSet {
return super.textAlignment placeholderLabel.textAlignment = textAlignment
} detailLabel.textAlignment = textAlignment
set(value) {
super.textAlignment = value
placeholderLabel.textAlignment = value
detailLabel.textAlignment = value
} }
} }
...@@ -420,13 +421,17 @@ open class TextField: UITextField { ...@@ -420,13 +421,17 @@ open class TextField: UITextField {
/// EdgeInsets for text. /// EdgeInsets for text.
@objc @objc
open var textInset: CGFloat = 0 open var textInsets: EdgeInsets = .zero
/// EdgeInsets preset property for text.
open var textInsetsPreset = EdgeInsetsPreset.none {
didSet {
textInsets = EdgeInsetsPresetToValue(preset: textInsetsPreset)
}
}
open override func textRect(forBounds bounds: CGRect) -> CGRect { open override func textRect(forBounds bounds: CGRect) -> CGRect {
var b = super.textRect(forBounds: bounds) return super.textRect(forBounds: bounds).inset(by: textInsets)
b.origin.x += textInset
b.size.width -= textInset
return b
} }
open override func editingRect(forBounds bounds: CGRect) -> CGRect { open override func editingRect(forBounds bounds: CGRect) -> CGRect {
...@@ -573,26 +578,29 @@ fileprivate extension TextField { ...@@ -573,26 +578,29 @@ fileprivate extension TextField {
fileprivate extension TextField { fileprivate extension TextField {
/// Layout the placeholderLabel. /// Layout the placeholderLabel.
func layoutPlaceholderLabel() { func layoutPlaceholderLabel() {
let x = leftViewWidth + textInset let leftPadding = leftViewWidth + textInsets.left
let h = 0 == bounds.height ? intrinsicContentSize.height : bounds.height let w = bounds.width - leftPadding - textInsets.right
let w = bounds.width - leftViewWidth - 2 * textInset var h = placeholderLabel.sizeThatFits(CGSize(width: w, height: .greatestFiniteMagnitude)).height
placeholderLabel.frame.size = CGSize(width: w, height: h) h = min(h, bounds.height - textInsets.top - textInsets.bottom)
h = max(h, minimumTextHeight)
placeholderLabel.bounds.size = CGSize(width: w, height: h)
guard isEditing || !isEmpty || !isPlaceholderAnimated else { guard isEditing || !isEmpty || !isPlaceholderAnimated else {
placeholderLabel.transform = CGAffineTransform.identity placeholderLabel.transform = CGAffineTransform.identity
placeholderLabel.frame.origin = CGPoint(x: x, y: 0) placeholderLabel.frame.origin = CGPoint(x: leftPadding, y: textInsets.top)
return return
} }
placeholderLabel.transform = CGAffineTransform(scaleX: placeholderActiveScale, y: placeholderActiveScale) placeholderLabel.transform = CGAffineTransform(scaleX: placeholderActiveScale, y: placeholderActiveScale)
placeholderLabel.frame.origin.y = -placeholderLabel.frame.height + placeholderVerticalOffset placeholderLabel.frame.origin.y = -placeholderLabel.frame.height + placeholderVerticalOffset
switch textAlignment { switch placeholderLabel.textAlignment {
case .left, .natural: case .left, .natural:
placeholderLabel.frame.origin.x = x + placeholderHorizontalOffset placeholderLabel.frame.origin.x = leftPadding + placeholderHorizontalOffset
case .right: case .right:
placeholderLabel.frame.origin.x = (bounds.width * (1.0 - placeholderActiveScale)) - textInset + placeholderHorizontalOffset let scaledWidth = w * placeholderActiveScale
placeholderLabel.frame.origin.x = bounds.width - scaledWidth - textInsets.right + placeholderHorizontalOffset
default:break default:break
} }
} }
......
...@@ -94,7 +94,7 @@ open class TextView: UITextView { ...@@ -94,7 +94,7 @@ open class TextView: UITextView {
} }
/// A boolean indicating whether the text is in edit mode. /// A boolean indicating whether the text is in edit mode.
open fileprivate(set) var isEditing = true open fileprivate(set) var isEditing = false
/// Is the keyboard hidden. /// Is the keyboard hidden.
open fileprivate(set) var isKeyboardHidden = true open fileprivate(set) var isKeyboardHidden = true
...@@ -134,6 +134,9 @@ open class TextView: UITextView { ...@@ -134,6 +134,9 @@ open class TextView: UITextView {
@IBInspectable @IBInspectable
public let placeholderLabel = UILabel() public let placeholderLabel = UILabel()
/// A property to enable/disable operations on the placeholderLabel
internal var isPlaceholderLabelEnabled: Bool = true
/// Placeholder normal text /// Placeholder normal text
@IBInspectable @IBInspectable
open var placeholderColor = Color.darkText.others { open var placeholderColor = Color.darkText.others {
...@@ -159,6 +162,13 @@ open class TextView: UITextView { ...@@ -159,6 +162,13 @@ open class TextView: UITextView {
} }
} }
/// Handles the textAlignment of the placeholderLabel and textView itself.
open override var textAlignment: NSTextAlignment {
didSet {
placeholderLabel.textAlignment = textAlignment
}
}
/** /**
An initializer that initializes the object with a NSCoder object. An initializer that initializes the object with a NSCoder object.
- Parameter aDecoder: A NSCoder instance. - Parameter aDecoder: A NSCoder instance.
...@@ -280,18 +290,70 @@ open class TextView: UITextView { ...@@ -280,18 +290,70 @@ open class TextView: UITextView {
prepareRegularExpression() prepareRegularExpression()
preparePlaceholderLabel() preparePlaceholderLabel()
} }
open override func insertText(_ text: String) { open override var contentSize: CGSize {
fixTypingFont() didSet {
super.insertText(text) guard isGrowEnabled else {
fixTypingFont() return
}
invalidateIntrinsicContentSize()
guard isEditing && isHeightChangeAnimated else {
superview?.layoutIfNeeded()
return
}
UIView.animate(withDuration: 0.15) {
let v = self.superview as? Editor ?? self
v.superview?.layoutIfNeeded()
}
} }
}
open override func paste(_ sender: Any?) {
fixTypingFont() /// A Boolean that indicates if the height change during growing is animated.
super.paste(sender) open var isHeightChangeAnimated = true
fixTypingFont()
/// Maximum preffered layout height before scrolling.
open var preferredMaxLayoutHeight: CGFloat = 0 {
didSet {
invalidateIntrinsicContentSize()
superview?.layoutIfNeeded()
} }
}
/// A property indicating if textView allowed to grow.
private var isGrowEnabled: Bool {
return preferredMaxLayoutHeight > 0 && isScrollEnabled
}
/// Minimum TextView text height.
internal let minimumTextHeight: CGFloat = 32
open override var intrinsicContentSize: CGSize {
guard isGrowEnabled else {
return super.intrinsicContentSize
}
let insets = textContainerInsets
let w = bounds.width - insets.left - insets.right - 2 * textContainer.lineFragmentPadding
let placeholderH = placeholderLabel.sizeThatFits(CGSize(width: w, height: .greatestFiniteMagnitude)).height
var h = max(minimumTextHeight, placeholderH) + insets.top + insets.bottom
h = max(h, contentSize.height)
return CGSize(width: UIView.noIntrinsicMetric, height: min(h, preferredMaxLayoutHeight))
}
open override func insertText(_ text: String) {
fixTypingFont()
super.insertText(text)
fixTypingFont()
}
open override func paste(_ sender: Any?) {
fixTypingFont()
super.paste(sender)
fixTypingFont()
}
} }
fileprivate extension TextView { fileprivate extension TextView {
...@@ -325,12 +387,20 @@ fileprivate extension TextView { ...@@ -325,12 +387,20 @@ fileprivate extension TextView {
fileprivate extension TextView { fileprivate extension TextView {
/// Updates the placeholderLabel text color. /// Updates the placeholderLabel text color.
func updatePlaceholderLabelColor() { func updatePlaceholderLabelColor() {
guard isPlaceholderLabelEnabled else {
return
}
tintColor = placeholderColor tintColor = placeholderColor
placeholderLabel.textColor = placeholderColor placeholderLabel.textColor = placeholderColor
} }
/// Updates the placeholderLabel visibility. /// Updates the placeholderLabel visibility.
func updatePlaceholderVisibility() { func updatePlaceholderVisibility() {
guard isPlaceholderLabelEnabled else {
return
}
placeholderLabel.isHidden = !isEmpty placeholderLabel.isHidden = !isEmpty
} }
} }
...@@ -338,15 +408,19 @@ fileprivate extension TextView { ...@@ -338,15 +408,19 @@ fileprivate extension TextView {
fileprivate extension TextView { fileprivate extension TextView {
/// Laysout the placeholder UILabel. /// Laysout the placeholder UILabel.
func layoutPlaceholderLabel() { func layoutPlaceholderLabel() {
placeholderLabel.preferredMaxLayoutWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2 guard isPlaceholderLabelEnabled else {
return
let x = textContainerInset.left + textContainer.lineFragmentPadding }
let y = textContainerInset.top
placeholderLabel.sizeToFit() let insets = textContainerInsets
let leftPadding = insets.left + textContainer.lineFragmentPadding
let rightPadding = insets.right + textContainer.lineFragmentPadding
let w = bounds.width - leftPadding - rightPadding
var h = placeholderLabel.sizeThatFits(CGSize(width: w, height: .greatestFiniteMagnitude)).height
h = max(h, minimumTextHeight)
h = min(h, bounds.height - insets.top - insets.bottom)
placeholderLabel.frame.origin.x = x placeholderLabel.frame = CGRect(x: leftPadding, y: insets.top, width: w, height: h)
placeholderLabel.frame.origin.y = y
placeholderLabel.frame.size.width = textContainer.size.width - textContainerInset.right - textContainer.lineFragmentPadding
} }
} }
......
...@@ -30,8 +30,6 @@ ...@@ -30,8 +30,6 @@
import UIKit import UIKit
fileprivate var ToolbarContext: UInt8 = 0
open class Toolbar: Bar { open class Toolbar: Bar {
/// A convenience property to set the titleLabel.text. /// A convenience property to set the titleLabel.text.
@IBInspectable @IBInspectable
...@@ -65,10 +63,6 @@ open class Toolbar: Bar { ...@@ -65,10 +63,6 @@ open class Toolbar: Bar {
@IBInspectable @IBInspectable
public let detailLabel = UILabel() public let detailLabel = UILabel()
deinit {
removeObserver(self, forKeyPath: #keyPath(titleLabel.textAlignment))
}
/** /**
An initializer that initializes the object with a NSCoder object. An initializer that initializes the object with a NSCoder object.
- Parameter aDecoder: A NSCoder instance. - Parameter aDecoder: A NSCoder instance.
...@@ -87,15 +81,6 @@ open class Toolbar: Bar { ...@@ -87,15 +81,6 @@ open class Toolbar: Bar {
super.init(frame: frame) super.init(frame: frame)
} }
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard "titleLabel.textAlignment" == keyPath else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
contentViewAlignment = .center == titleLabel.textAlignment ? .center : .full
}
open override func layoutSubviews() { open override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
guard willLayout else { guard willLayout else {
...@@ -143,16 +128,21 @@ open class Toolbar: Bar { ...@@ -143,16 +128,21 @@ open class Toolbar: Bar {
prepareTitleLabel() prepareTitleLabel()
prepareDetailLabel() prepareDetailLabel()
} }
/// A reference to titleLabel.textAlignment observation.
private var titleLabelTextAlignmentObserver: NSKeyValueObservation!
} }
fileprivate extension Toolbar { private extension Toolbar {
/// Prepares the titleLabel. /// Prepares the titleLabel.
func prepareTitleLabel() { func prepareTitleLabel() {
titleLabel.textAlignment = .center titleLabel.textAlignment = .center
titleLabel.contentScaleFactor = Screen.scale titleLabel.contentScaleFactor = Screen.scale
titleLabel.font = RobotoFont.medium(with: 17) titleLabel.font = RobotoFont.medium(with: 17)
titleLabel.textColor = Color.darkText.primary titleLabel.textColor = Color.darkText.primary
addObserver(self, forKeyPath: #keyPath(titleLabel.textAlignment), options: [], context: &ToolbarContext) titleLabelTextAlignmentObserver = titleLabel.observe(\.textAlignment) { [weak self] titleLabel, _ in
self?.contentViewAlignment = .center == titleLabel.textAlignment ? .center : .full
}
} }
/// Prepares the detailLabel. /// Prepares the detailLabel.
......
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