Files
André Pacheco Neves 739a4f5d78 Fix skeletons interfering with attributed strings in text-based views (#520)
When enabling skeleton mode in a text-based view (`UILabel`,
`UITextView`, `UITextField`), it sets the `textColor` to `.clear`,
which is fine when `text` is used, but causes problems when
`attributedText` is used, as it effectively "resets" the string to have
a single color.

Additionally, when a `UILabel` is nested inside a `UIStackView` a
dummy string `" "` was set on the label's `text` so that it didn't have
a 0 height content size. However, this workaround didn't consider the
case where the label already had a non-empty text, meaning that this
(intrusive) `text = " "` broke existing code by clearing the label's
contents.

By improving the corresponding `RecoverableXState` structs, we are able
to preserve each element's contents and state as skeleton is disabled.

Fixes #518.

## Changes

- Create new `RecoverableLabelState` containing a `attributedText` and
`text`, and use it on `UILabel`.

- Update `RecoverableTextViewState` and `RecoverableTextFieldState` to
have a `attributedText`.

- Check if `UILabel`'s `text` is empty before setting dummy value when
enabling skeleton mode in a label nested inside a `UIStackView`.
2022-10-20 12:59:00 +02:00

208 lines
6.5 KiB
Swift

//
// Recoverable.swift
// SkeletonView
//
// Created by Juanpe Catalán on 13/05/2018.
// Copyright © 2018 SkeletonView. All rights reserved.
//
import UIKit
protocol Recoverable {
func saveViewState()
func recoverViewState(forced: Bool)
}
extension UIView: Recoverable {
var viewState: RecoverableViewState? {
get { return ao_get(pkey: &ViewAssociatedKeys.viewState) as? RecoverableViewState }
set { ao_setOptional(newValue, pkey: &ViewAssociatedKeys.viewState) }
}
@objc func saveViewState() {
viewState = RecoverableViewState(view: self)
}
@objc func recoverViewState(forced: Bool) {
guard let storedViewState = viewState else { return }
startTransition { [weak self] in
guard let self = self else { return }
self.layer.cornerRadius = storedViewState.cornerRadius
self.layer.masksToBounds = storedViewState.clipToBounds
if self.isUserInteractionDisabledWhenSkeletonIsActive {
self.isUserInteractionEnabled = storedViewState.isUserInteractionsEnabled
}
if self.backgroundColor == .clear || forced {
self.backgroundColor = storedViewState.backgroundColor
}
}
}
}
extension UILabel {
var labelState: RecoverableLabelState? {
get { return ao_get(pkey: &ViewAssociatedKeys.labelViewState) as? RecoverableLabelState }
set { ao_setOptional(newValue, pkey: &ViewAssociatedKeys.labelViewState) }
}
override func saveViewState() {
super.saveViewState()
labelState = RecoverableLabelState(view: self)
}
override func recoverViewState(forced: Bool) {
super.recoverViewState(forced: forced)
startTransition { [weak self] in
guard let self = self,
let storedLabelState = self.labelState else {
return
}
NSLayoutConstraint.deactivate(self.skeletonHeightConstraints)
self.restoreBackupHeightConstraintsIfNeeded()
if self.textColor == .clear || forced {
self.textColor = storedLabelState.textColor
if let attributedText = storedLabelState.attributedText {
self.attributedText = attributedText
} else {
self.text = storedLabelState.text
}
}
}
}
}
extension UITextView {
var textState: RecoverableTextViewState? {
get { return ao_get(pkey: &ViewAssociatedKeys.labelViewState) as? RecoverableTextViewState }
set { ao_setOptional(newValue, pkey: &ViewAssociatedKeys.labelViewState) }
}
override func saveViewState() {
super.saveViewState()
textState = RecoverableTextViewState(view: self)
}
override func recoverViewState(forced: Bool) {
super.recoverViewState(forced: forced)
startTransition { [weak self] in
guard let storedLabelState = self?.textState else { return }
if self?.textColor == .clear || forced {
self?.textColor = storedLabelState.textColor
if let attributedText = storedLabelState.attributedText {
self?.attributedText = attributedText
}
}
}
}
}
extension UITextField {
var textState: RecoverableTextFieldState? {
get { return ao_get(pkey: &ViewAssociatedKeys.labelViewState) as? RecoverableTextFieldState }
set { ao_setOptional(newValue, pkey: &ViewAssociatedKeys.labelViewState) }
}
override func saveViewState() {
super.saveViewState()
textState = RecoverableTextFieldState(view: self)
}
override func recoverViewState(forced: Bool) {
super.recoverViewState(forced: forced)
startTransition { [weak self] in
guard let storedLabelState = self?.textState else { return }
if self?.textColor == .clear || forced {
self?.textColor = storedLabelState.textColor
if let attributedText = storedLabelState.attributedText {
self?.attributedText = attributedText
}
}
if self?.placeholder == nil || forced {
self?.placeholder = storedLabelState.placeholder
}
}
}
}
extension UIImageView {
var imageState: RecoverableImageViewState? {
get { return ao_get(pkey: &ViewAssociatedKeys.imageViewState) as? RecoverableImageViewState }
set { ao_setOptional(newValue, pkey: &ViewAssociatedKeys.imageViewState) }
}
override func saveViewState() {
super.saveViewState()
imageState = RecoverableImageViewState(view: self)
}
override func recoverViewState(forced: Bool) {
super.recoverViewState(forced: forced)
startTransition { [weak self] in
self?.image = self?.image == nil || forced ? self?.imageState?.image : self?.image
}
}
}
extension UIButton {
var buttonState: RecoverableButtonViewState? {
get { return ao_get(pkey: &ViewAssociatedKeys.buttonViewState) as? RecoverableButtonViewState }
set { ao_setOptional(newValue, pkey: &ViewAssociatedKeys.buttonViewState) }
}
override func saveViewState() {
super.saveViewState()
buttonState = RecoverableButtonViewState(view: self)
}
override func recoverViewState(forced: Bool) {
super.recoverViewState(forced: forced)
startTransition { [weak self] in
if self?.title(for: .normal) == nil {
self?.setTitle(self?.buttonState?.title, for: .normal)
}
}
}
}
extension UITableViewHeaderFooterView {
var headerFooterState: RecoverableTableViewHeaderFooterViewState? {
get { return ao_get(pkey: &ViewAssociatedKeys.headerFooterViewState) as? RecoverableTableViewHeaderFooterViewState }
set { ao_setOptional(newValue, pkey: &ViewAssociatedKeys.headerFooterViewState) }
}
override func saveViewState() {
super.saveViewState()
headerFooterState = RecoverableTableViewHeaderFooterViewState(view: self)
}
override func recoverViewState(forced: Bool) {
super.recoverViewState(forced: forced)
startTransition { [weak self] in
self?.backgroundView?.backgroundColor = self?.headerFooterState?.backgroundViewColor
}
}
}