diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2021-02-22 12:45:28 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2021-02-24 11:19:07 +0100 |
| commit | d3816ce772bf1a050f8a33d73e25ed3efec7c885 (patch) | |
| tree | 42a5d4c004baf0a396d8da27099fa6a604f99e65 | |
| parent | 1cf9b1e0a2c4f319eccdc7ca05ee2134f0ee13d9 (diff) | |
| download | mullvadvpn-d3816ce772bf1a050f8a33d73e25ed3efec7c885.tar.xz mullvadvpn-d3816ce772bf1a050f8a33d73e25ed3efec7c885.zip | |
Add custom text field & text view
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 8 | ||||
| -rw-r--r-- | ios/MullvadVPN/CustomTextField.swift | 64 | ||||
| -rw-r--r-- | ios/MullvadVPN/CustomTextView.swift | 124 | ||||
| -rw-r--r-- | ios/MullvadVPN/UIColor+Palette.swift | 5 |
4 files changed, 201 insertions, 0 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index d3c509d4b6..4ccdcc0649 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -51,6 +51,8 @@ 581503A624D6F4AE00C9C50E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A524D6F4AE00C9C50E /* Logging.swift */; }; 581503A724D6F4AE00C9C50E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A524D6F4AE00C9C50E /* Logging.swift */; }; 581CBCEE229826FD00727D7F /* StaticTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581CBCED229826FD00727D7F /* StaticTableViewDataSource.swift */; }; + 58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB025124117005D0BB5 /* CustomTextField.swift */; }; + 58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB2251241B3005D0BB5 /* CustomTextView.swift */; }; 582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */; }; 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */; }; 582AE3132440CA2700E6733A /* AccountTokenInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */; }; @@ -284,6 +286,8 @@ 581503A224D6F1EC00C9C50E /* ChainedError+Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChainedError+Logger.swift"; sourceTree = "<group>"; }; 581503A524D6F4AE00C9C50E /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; }; 581CBCED229826FD00727D7F /* StaticTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticTableViewDataSource.swift; sourceTree = "<group>"; }; + 58293FB025124117005D0BB5 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; }; + 58293FB2251241B3005D0BB5 /* CustomTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextView.swift; sourceTree = "<group>"; }; 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountTokenInput.swift; sourceTree = "<group>"; }; 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTokenInputTests.swift; sourceTree = "<group>"; }; 582BB1AE229566420055B6EF /* SettingsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = "<group>"; }; @@ -547,6 +551,8 @@ 58AB9DEB2501040C006C5526 /* ConsentViewController.xib */, 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */, 582BB1B0229569620055B6EF /* CustomNavigationBar.swift */, + 58293FB025124117005D0BB5 /* CustomTextField.swift */, + 58293FB2251241B3005D0BB5 /* CustomTextView.swift */, 58C6B35D22BBBFE3003C19AD /* Data+HexCoding.swift */, 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */, 58B9EB142489139B00095626 /* DisplayChainedError.swift */, @@ -981,11 +987,13 @@ 58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */, 58CB0EE024B86751001EF0D8 /* MullvadRest.swift in Sources */, 580EE20924B3224200F9D8A1 /* RetryOperation.swift in Sources */, + 58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */, 582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */, 58FAEDF7245088E100CB0F5B /* Keychain.swift in Sources */, 5871FB8325498CA20051A0A4 /* Swizzle.swift in Sources */, 58907D9524D17B4E00CFC3F5 /* DisconnectSplitButton.swift in Sources */, 5888AD87227B17950051EB06 /* SelectLocationViewController.swift in Sources */, + 58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */, 580EE20424B321EC00F9D8A1 /* OperationObserver.swift in Sources */, 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */, 58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */, diff --git a/ios/MullvadVPN/CustomTextField.swift b/ios/MullvadVPN/CustomTextField.swift new file mode 100644 index 0000000000..378dde07aa --- /dev/null +++ b/ios/MullvadVPN/CustomTextField.swift @@ -0,0 +1,64 @@ +// +// CustomTextField.swift +// MullvadVPN +// +// Created by pronebird on 16/09/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import UIKit + +private let kTextFieldCornerRadius = CGFloat(4) + +class CustomTextField: UITextField { + + var placeholderTextColor: UIColor = UIColor.TextField.placeholderTextColor { + didSet { + updatePlaceholderTextColor() + } + } + + override var placeholder: String? { + didSet { + updatePlaceholderTextColor() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + textColor = UIColor.TextField.textColor + layer.cornerRadius = kTextFieldCornerRadius + clipsToBounds = true + } + + override func didAddSubview(_ subview: UIView) { + super.didAddSubview(subview) + + // Internally `UITextField` adds the placeholder label to its view hierarchy. + // Intercept it here and update the text color. + if let placeholderLabel = subview as? UILabel { + placeholderLabel.textColor = placeholderTextColor + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func textRect(forBounds bounds: CGRect) -> CGRect { + return bounds.insetBy(dx: 14, dy: 12) + } + + override func editingRect(forBounds bounds: CGRect) -> CGRect { + return textRect(forBounds: bounds) + } + + private func updatePlaceholderTextColor() { + for case let placeholderLabel as UILabel in subviews { + placeholderLabel.textColor = placeholderTextColor + break + } + } +} diff --git a/ios/MullvadVPN/CustomTextView.swift b/ios/MullvadVPN/CustomTextView.swift new file mode 100644 index 0000000000..3ce1fa9466 --- /dev/null +++ b/ios/MullvadVPN/CustomTextView.swift @@ -0,0 +1,124 @@ +// +// CustomTextView.swift +// MullvadVPN +// +// Created by pronebird on 16/09/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import UIKit + +private let kTextViewCornerRadius = CGFloat(4) + +class CustomTextView: UITextView { + + var roundCorners: Bool = true { + didSet { + layer.cornerRadius = roundCorners ? kTextViewCornerRadius : 0 + } + } + + /// Placeholder string + var placeholder: String? { + set { + placeholderTextLabel.text = newValue + } + get { + return placeholderTextLabel.text + } + } + + /// Placeholder text label + private let placeholderTextLabel = UILabel() + + /// Placeholder label constraints + private var placeholderConstraints = [NSLayoutConstraint]() + + override var textContainerInset: UIEdgeInsets { + didSet { + setNeedsUpdateConstraints() + } + } + + override var font: UIFont? { + didSet { + placeholderTextLabel.font = self.font ?? UIFont.preferredFont(forTextStyle: .body) + } + } + + /// Placeholder text inset derived from `textContainerInset` + private var placeholderTextInset: UIEdgeInsets { + var placeholderInset = textContainerInset + + // Offset the placeholder label to match with text view rendering. + placeholderInset.top += 0.5 + + return placeholderInset + } + + override init(frame: CGRect, textContainer: NSTextContainer?) { + super.init(frame: frame, textContainer: textContainer) + + placeholderTextLabel.textColor = UIColor.TextField.placeholderTextColor + placeholderTextLabel.highlightedTextColor = UIColor.TextField.placeholderTextColor + placeholderTextLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(placeholderTextLabel) + + // Create placeholder constraints + placeholderConstraints = [ + placeholderTextLabel.topAnchor.constraint(equalTo: topAnchor), + placeholderTextLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + placeholderTextLabel.trailingAnchor.constraint(equalTo: trailingAnchor) + ] + NSLayoutConstraint.activate(placeholderConstraints) + + // Set visual appearance + textColor = UIColor.TextField.textColor + layer.cornerRadius = kTextViewCornerRadius + clipsToBounds = true + + // Set content padding + contentInset = .zero + textContainerInset = UIEdgeInsets(top: 12, left: 14, bottom: 12, right: 14) + self.textContainer.lineFragmentPadding = 0 + + // Handle placeholder visibility + NotificationCenter.default.addObserver( + forName: NSTextStorage.didProcessEditingNotification, + object: textStorage, + queue: OperationQueue.main) { [weak self] (note) in + self?.updatePlaceholderVisibility() + } + + updatePlaceholderVisibility() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateConstraints() { + let textInset = placeholderTextInset + + for constraint in placeholderConstraints { + switch constraint.firstAttribute { + case .top: + constraint.constant = textInset.top + case .leading: + constraint.constant = textInset.left + case .trailing: + constraint.constant = textInset.right + default: + break + } + } + + super.updateConstraints() + } + + private func updatePlaceholderVisibility() { + placeholderTextLabel.isHidden = textStorage.length > 0 + } + +} diff --git a/ios/MullvadVPN/UIColor+Palette.swift b/ios/MullvadVPN/UIColor+Palette.swift index 9f9e64f27f..f31f67c9fb 100644 --- a/ios/MullvadVPN/UIColor+Palette.swift +++ b/ios/MullvadVPN/UIColor+Palette.swift @@ -30,6 +30,11 @@ extension UIColor { } } + enum TextField { + static let placeholderTextColor = UIColor(red: 0.16, green: 0.30, blue: 0.45, alpha: 0.40) + static let textColor = UIColor(red: 0.16, green: 0.30, blue: 0.45, alpha: 1.00) + } + enum AppButton { static let normalTitleColor = UIColor.white static let highlightedTitleColor = UIColor.lightGray |
