Commit 8a9d0902 by Orkhan Alikhanov Committed by OrkhanAlikhanov

Added Editor component

parent edf428b6
...@@ -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
/// 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()
prepareTextView()
preparePlaceholderLabel()
prepareNotificationHandlers()
}
open override func layoutSubviews() {
super.layoutSubviews()
layoutPlaceholderLabel()
}
}
private extension Editor {
/// 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 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
}
}
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 = min(h, bounds.height - inset.top - inset.bottom)
guard isEditing || !isEmpty || !isPlaceholderAnimated else {
placeholderLabel.transform = CGAffineTransform.identity
placeholderLabel.frame = CGRect(x: leftPadding, y: inset.top, width: w, height: h)
return
}
placeholderLabel.transform = CGAffineTransform(scaleX: placeholderActiveScale, y: placeholderActiveScale)
placeholderLabel.frame.size = CGSize(width: w * placeholderActiveScale, height: h * 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
}
}
}
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)
}
func updateEditorState(animated: Bool = false) {
updatePlaceholderVisibility()
updatePlaceholderLabelColor()
guard isPlaceholderAnimated else {
layoutPlaceholderLabel()
return
}
UIView.animate(withDuration: 0.15, animations: layoutPlaceholderLabel)
}
}
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