Clean up and fix layout issues in Edit Avatar screen. (#11990)
Stop using OWSTableViewController2. Instead use OWSViewController + UIScrollView. This change is prompted by removing obsolete method of tracking keyboard frame.
This commit is contained in:
parent
b0366644ab
commit
1b82b2aedb
@ -6,12 +6,14 @@
|
||||
import SignalServiceKit
|
||||
import SignalUI
|
||||
|
||||
class AvatarEditViewController: OWSTableViewController2 {
|
||||
class AvatarEditViewController: OWSViewController {
|
||||
private let originalModel: AvatarModel
|
||||
private var model: AvatarModel {
|
||||
didSet {
|
||||
updateHeaderView()
|
||||
updateNavigation()
|
||||
if isViewLoaded {
|
||||
updateHeaderViewState()
|
||||
}
|
||||
}
|
||||
}
|
||||
private let completion: (AvatarModel) -> Void
|
||||
@ -22,10 +24,8 @@ class AvatarEditViewController: OWSTableViewController2 {
|
||||
self.originalModel = model
|
||||
self.model = model
|
||||
self.completion = completion
|
||||
|
||||
super.init()
|
||||
createTopHeader()
|
||||
createBottomFooter()
|
||||
shouldAvoidKeyboard = true
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
@ -36,8 +36,37 @@ class AvatarEditViewController: OWSTableViewController2 {
|
||||
super.viewDidLoad()
|
||||
|
||||
updateNavigation()
|
||||
updateHeaderView()
|
||||
updateFooterView()
|
||||
updateHeaderViewState()
|
||||
updateFooterViewState()
|
||||
|
||||
let scrollView = UIScrollView()
|
||||
view.addSubview(scrollView)
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
||||
scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
|
||||
scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
|
||||
scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -8),
|
||||
])
|
||||
|
||||
scrollView.addSubview(topHeader)
|
||||
topHeader.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
topHeader.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
|
||||
topHeader.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
|
||||
topHeader.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
|
||||
topHeader.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
|
||||
])
|
||||
|
||||
scrollView.addSubview(bottomFooterStack)
|
||||
bottomFooterStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
bottomFooterStack.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
|
||||
bottomFooterStack.topAnchor.constraint(equalTo: topHeader.bottomAnchor),
|
||||
bottomFooterStack.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
|
||||
bottomFooterStack.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
|
||||
bottomFooterStack.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
override func themeDidChange() {
|
||||
@ -80,10 +109,8 @@ class AvatarEditViewController: OWSTableViewController2 {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
coordinator.animate { [weak self] _ in
|
||||
self?.updateHeaderView()
|
||||
self?.updateFooterViewLayout()
|
||||
} completion: { [weak self] _ in
|
||||
self?.updateHeaderView()
|
||||
self?.updateFooterViewLayout()
|
||||
}
|
||||
}
|
||||
@ -137,58 +164,67 @@ class AvatarEditViewController: OWSTableViewController2 {
|
||||
switch selectedSegment {
|
||||
case .color:
|
||||
headerTextField.resignFirstResponder()
|
||||
updateFooterView()
|
||||
updateFooterViewState()
|
||||
case .text:
|
||||
headerTextField.becomeFirstResponder()
|
||||
updateFooterView()
|
||||
updateFooterViewState()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Header
|
||||
|
||||
private let headerImageView = AvatarImageView()
|
||||
private let headerTextField = UITextField()
|
||||
private let topHeaderStack = UIStackView()
|
||||
private func createTopHeader() {
|
||||
topHeaderStack.isLayoutMarginsRelativeArrangement = true
|
||||
topHeaderStack.axis = .vertical
|
||||
topHeaderStack.alignment = .center
|
||||
|
||||
let topSpacer = UIView.vStretchingSpacer()
|
||||
topHeaderStack.addArrangedSubview(topSpacer)
|
||||
|
||||
headerTextField.adjustsFontSizeToFitWidth = true
|
||||
headerTextField.textAlignment = .center
|
||||
headerTextField.delegate = self
|
||||
headerTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
|
||||
headerTextField.returnKeyType = .done
|
||||
headerTextField.autocorrectionType = .no
|
||||
headerTextField.spellCheckingType = .no
|
||||
private lazy var headerImageView: AvatarImageView = {
|
||||
let imageView = AvatarImageView()
|
||||
imageView.isUserInteractionEnabled = true
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return imageView
|
||||
}()
|
||||
private lazy var headerTextField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.adjustsFontSizeToFitWidth = true
|
||||
textField.textAlignment = .center
|
||||
textField.delegate = self
|
||||
textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
|
||||
textField.returnKeyType = .done
|
||||
textField.autocorrectionType = .no
|
||||
textField.spellCheckingType = .no
|
||||
textField.isHidden = true
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
return textField
|
||||
}()
|
||||
private lazy var topHeader: UIView = {
|
||||
headerImageView.addSubview(headerTextField)
|
||||
headerTextField.autoPinEdgesToSuperviewEdges(with: AvatarBuilder.avatarTextMargins(diameter: Self.headerAvatarSize))
|
||||
headerTextField.isHidden = true
|
||||
let insets = AvatarBuilder.avatarTextMargins(diameter: Self.headerAvatarSize)
|
||||
NSLayoutConstraint.activate([
|
||||
headerImageView.widthAnchor.constraint(equalToConstant: Self.headerAvatarSize),
|
||||
headerImageView.heightAnchor.constraint(equalToConstant: Self.headerAvatarSize),
|
||||
headerTextField.leadingAnchor.constraint(equalTo: headerImageView.leadingAnchor, constant: insets.left),
|
||||
headerTextField.trailingAnchor.constraint(equalTo: headerImageView.trailingAnchor, constant: -insets.right),
|
||||
headerTextField.topAnchor.constraint(equalTo: headerImageView.topAnchor, constant: insets.top),
|
||||
headerTextField.bottomAnchor.constraint(equalTo: headerImageView.bottomAnchor, constant: -insets.bottom),
|
||||
])
|
||||
|
||||
headerImageView.autoSetDimensions(to: CGSize(square: Self.headerAvatarSize))
|
||||
headerImageView.isUserInteractionEnabled = true
|
||||
topHeaderStack.addArrangedSubview(headerImageView)
|
||||
let view = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(headerImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
headerImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
headerImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
headerImageView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor),
|
||||
headerImageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 32),
|
||||
])
|
||||
|
||||
let bottomSpacer = UIView.vStretchingSpacer()
|
||||
topHeaderStack.addArrangedSubview(bottomSpacer)
|
||||
topSpacer.autoMatch(.height, to: .height, of: bottomSpacer)
|
||||
topSpacer.autoSetDimension(.height, toSize: 16, relation: .greaterThanOrEqual)
|
||||
topSpacer.setCompressionResistanceLow()
|
||||
bottomSpacer.setCompressionResistanceLow()
|
||||
|
||||
topHeader = topHeaderStack
|
||||
}
|
||||
|
||||
func updateHeaderView() {
|
||||
topHeaderStack.layoutMargins = cellOuterInsets
|
||||
return view
|
||||
}()
|
||||
|
||||
private func updateHeaderViewState() {
|
||||
switch model.type {
|
||||
case .icon:
|
||||
headerTextField.isHidden = true
|
||||
headerImageView.image = SSKEnvironment.shared.avatarBuilderRef.avatarImage(model: model, diameterPoints: UInt(Self.headerAvatarSize))
|
||||
headerImageView.image = SSKEnvironment.shared.avatarBuilderRef.avatarImage(
|
||||
model: model,
|
||||
diameterPoints: UInt(Self.headerAvatarSize)
|
||||
)
|
||||
case .text(let text):
|
||||
headerTextField.isHidden = false
|
||||
headerTextField.textColor = model.theme.foregroundColor
|
||||
@ -205,29 +241,55 @@ class AvatarEditViewController: OWSTableViewController2 {
|
||||
|
||||
// MARK: - Footer View
|
||||
|
||||
private let bottomFooterStack = UIStackView()
|
||||
private let segmentedControlContainer = UIView()
|
||||
private let themePickerContainer = UIView()
|
||||
private let themeHeaderContainer = UIView()
|
||||
private lazy var bottomFooterStack: UIStackView = {
|
||||
let stackView = UIStackView(arrangedSubviews: [
|
||||
segmentedControlContainer,
|
||||
themeHeaderContainer,
|
||||
themePickerContainer,
|
||||
])
|
||||
stackView.axis = .vertical
|
||||
stackView.setCustomSpacing(16, after: segmentedControlContainer)
|
||||
return stackView
|
||||
}()
|
||||
private lazy var segmentedControlContainer: UIView = {
|
||||
let container = UIView()
|
||||
container.addSubview(segmentedControl)
|
||||
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
segmentedControl.centerXAnchor.constraint(equalTo: container.centerXAnchor),
|
||||
segmentedControl.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||
segmentedControl.topAnchor.constraint(equalTo: container.topAnchor, constant: 8),
|
||||
segmentedControl.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -8),
|
||||
])
|
||||
return container
|
||||
}()
|
||||
private lazy var themePickerContainer = UIView()
|
||||
private lazy var themeHeaderContainer: UIView = {
|
||||
let label = UILabel()
|
||||
label.text = OWSLocalizedString(
|
||||
"AVATAR_EDIT_VIEW_CHOOSE_A_COLOR",
|
||||
comment: "Text prompting the user to choose a color when editing their avatar"
|
||||
)
|
||||
label.textColor = .Signal.label
|
||||
label.font = UIFont.dynamicTypeBodyClamped.semibold()
|
||||
|
||||
private func createBottomFooter() {
|
||||
bottomFooterStack.isLayoutMarginsRelativeArrangement = true
|
||||
bottomFooterStack.axis = .vertical
|
||||
bottomFooterStack.spacing = 16
|
||||
|
||||
segmentedControlContainer.addSubview(segmentedControl)
|
||||
segmentedControl.autoPinEdgesToSuperviewEdges()
|
||||
bottomFooterStack.addArrangedSubview(segmentedControlContainer)
|
||||
|
||||
bottomFooterStack.addArrangedSubview(themeHeaderContainer)
|
||||
bottomFooterStack.addArrangedSubview(themePickerContainer)
|
||||
bottomFooterStack.addArrangedSubview(.vStretchingSpacer())
|
||||
|
||||
bottomFooter = bottomFooterStack
|
||||
}
|
||||
|
||||
private func updateFooterView() {
|
||||
let view = UIView()
|
||||
view.layoutMargins = UIEdgeInsets(
|
||||
hMargin: OWSTableViewController2.cellHInnerMargin * 0.5,
|
||||
vMargin: 8
|
||||
)
|
||||
view.addSubview(label)
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
label.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
||||
label.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
|
||||
label.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
|
||||
label.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
|
||||
])
|
||||
return view
|
||||
}()
|
||||
|
||||
private func updateFooterViewState() {
|
||||
if case .text = model.type {
|
||||
segmentedControlContainer.isHiddenInStackView = false
|
||||
themeHeaderContainer.isHiddenInStackView = true
|
||||
@ -243,25 +305,20 @@ class AvatarEditViewController: OWSTableViewController2 {
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
updateFooterViewLayout()
|
||||
}
|
||||
|
||||
override func viewLayoutMarginsDidChange() {
|
||||
super.viewLayoutMarginsDidChange()
|
||||
updateFooterViewLayout()
|
||||
}
|
||||
|
||||
private var previousSizeReference: CGFloat?
|
||||
private func updateFooterViewLayout(forceUpdate: Bool = false) {
|
||||
// Update theme options layout only when the view size changes.
|
||||
guard view.width != previousSizeReference || forceUpdate else { return }
|
||||
previousSizeReference = view.width
|
||||
guard view.readableContentGuide.layoutFrame.width != previousSizeReference || forceUpdate else { return }
|
||||
previousSizeReference = view.readableContentGuide.layoutFrame.width
|
||||
|
||||
let hMargin = Self.cellOuterInset(in: view)
|
||||
bottomFooterStack.layoutMargins = .init(
|
||||
top: 12,
|
||||
left: hMargin,
|
||||
bottom: 10,
|
||||
right: hMargin
|
||||
)
|
||||
|
||||
updateThemeHeaderContainer()
|
||||
updateThemePickerContainer()
|
||||
}
|
||||
|
||||
@ -277,27 +334,16 @@ class AvatarEditViewController: OWSTableViewController2 {
|
||||
return optionView
|
||||
}
|
||||
|
||||
private func updateThemeHeaderContainer() {
|
||||
themeHeaderContainer.removeAllSubviews()
|
||||
|
||||
let label = UILabel()
|
||||
label.text = OWSLocalizedString(
|
||||
"AVATAR_EDIT_VIEW_CHOOSE_A_COLOR",
|
||||
comment: "Text prompting the user to choose a color when editing their avatar"
|
||||
)
|
||||
label.textColor = Theme.isDarkThemeEnabled ? .ows_gray05 : .ows_gray90
|
||||
label.font = UIFont.dynamicTypeBodyClamped.semibold()
|
||||
themeHeaderContainer.addSubview(label)
|
||||
label.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(hMargin: Self.cellHInnerMargin * 0.5, vMargin: 0))
|
||||
}
|
||||
|
||||
private func updateThemePickerContainer() {
|
||||
themePickerContainer.removeAllSubviews()
|
||||
themePickerContainer.layoutMargins = UIEdgeInsets(hMargin: Self.cellHInnerMargin, vMargin: Self.cellVInnerMargin)
|
||||
themePickerContainer.backgroundColor = cellBackgroundColor
|
||||
themePickerContainer.layer.cornerRadius = Self.cellRounding
|
||||
themePickerContainer.layoutMargins = UIEdgeInsets(
|
||||
hMargin: OWSTableViewController2.cellHInnerMargin,
|
||||
vMargin: OWSTableViewController2.cellVInnerMargin
|
||||
)
|
||||
themePickerContainer.backgroundColor = Theme.tableCell2PresentedBackgroundColor
|
||||
themePickerContainer.layer.cornerRadius = OWSTableViewController2.cellRounding
|
||||
|
||||
let rowWidth = max(0, view.width - (view.safeAreaInsets.totalWidth + cellOuterInsets.totalWidth + Self.cellHInnerMargin * 2))
|
||||
let rowWidth = max(0, view.readableContentGuide.layoutFrame.width - OWSTableViewController2.cellHInnerMargin * 2)
|
||||
let themeSpacing: CGFloat = 16
|
||||
let minThemeSize: CGFloat = min(66, (rowWidth - (themeSpacing * 3)) / 4)
|
||||
let themesPerRow = max(1, Int(floor(rowWidth + themeSpacing) / (minThemeSize + themeSpacing)))
|
||||
@ -338,7 +384,7 @@ extension AvatarEditViewController: UITextFieldDelegate {
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
updateFooterView()
|
||||
updateFooterViewState()
|
||||
return false
|
||||
}
|
||||
|
||||
@ -350,12 +396,12 @@ extension AvatarEditViewController: UITextFieldDelegate {
|
||||
|
||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
segmentedControl.selectedSegmentIndex = Segments.text.rawValue
|
||||
updateFooterView()
|
||||
updateFooterViewState()
|
||||
}
|
||||
|
||||
func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
segmentedControl.selectedSegmentIndex = Segments.color.rawValue
|
||||
updateFooterView()
|
||||
updateFooterViewState()
|
||||
}
|
||||
}
|
||||
|
||||
@ -373,7 +419,6 @@ private protocol OptionViewDelegate: AnyObject {
|
||||
private class OptionView: UIView {
|
||||
private weak var delegate: OptionViewDelegate?
|
||||
private let colorView = UIView()
|
||||
private var colorViewInsetConstraints: [NSLayoutConstraint]?
|
||||
|
||||
var isSelected = false {
|
||||
didSet {
|
||||
@ -387,7 +432,16 @@ private class OptionView: UIView {
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
layoutMargins = .zero
|
||||
|
||||
addSubview(colorView)
|
||||
colorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
colorView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||
colorView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
||||
colorView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||
colorView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
|
||||
])
|
||||
updateSelectionState()
|
||||
|
||||
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
|
||||
@ -406,24 +460,21 @@ private class OptionView: UIView {
|
||||
|
||||
@objc
|
||||
private func handleTap() {
|
||||
guard let theme = theme else {
|
||||
guard let theme else {
|
||||
return owsFailDebug("Unexpectedly missing theme in OptionView")
|
||||
}
|
||||
|
||||
if !isSelected {
|
||||
isSelected = true
|
||||
delegate?.didSelectOptionView(self, theme: theme)
|
||||
}
|
||||
guard !isSelected else { return }
|
||||
|
||||
isSelected = true
|
||||
delegate?.didSelectOptionView(self, theme: theme)
|
||||
}
|
||||
|
||||
func updateSelectionState() {
|
||||
colorViewInsetConstraints?.forEach { $0.isActive = false }
|
||||
colorViewInsetConstraints = colorView.autoPinEdgesToSuperviewEdges(
|
||||
with: isSelected ? UIEdgeInsets(margin: 4) : .zero
|
||||
)
|
||||
private func updateSelectionState() {
|
||||
layoutMargins = isSelected ? UIEdgeInsets(margin: 4) : .zero
|
||||
|
||||
if isSelected {
|
||||
layer.borderColor = Theme.primaryTextColor.cgColor
|
||||
layer.borderColor = UIColor.Signal.label.cgColor
|
||||
layer.borderWidth = 2.5
|
||||
} else {
|
||||
layer.borderColor = nil
|
||||
@ -431,11 +482,16 @@ private class OptionView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
func updateTheme() {
|
||||
private func updateTheme() {
|
||||
colorView.backgroundColor = theme?.backgroundColor
|
||||
}
|
||||
|
||||
private var theme: AvatarTheme? { didSet { updateTheme() }}
|
||||
private var theme: AvatarTheme? {
|
||||
didSet {
|
||||
updateTheme()
|
||||
}
|
||||
}
|
||||
|
||||
func configure(theme: AvatarTheme, isSelected: Bool) {
|
||||
self.theme = theme
|
||||
self.isSelected = isSelected
|
||||
|
||||
Loading…
Reference in New Issue
Block a user