Skip to content

Commit

Permalink
Merge pull request #4906 from wikimedia/alt-text-sheet-first-part
Browse files Browse the repository at this point in the history
[ALT TEXT] Bottom sheet - part 1
  • Loading branch information
tonisevener authored Aug 1, 2024
2 parents 5d21850 + fc65dcc commit 35d2ef3
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import UIKit

final class AltTextExperimentModalSheetView: WKComponentView {

// MARK: Properties

weak var viewModel: AltTextExperimentModalSheetViewModel?

private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()

private lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.setContentHuggingPriority(.required, for: .vertical)
stackView.setContentCompressionResistancePriority(.required, for: .vertical)
stackView.alignment = .fill
stackView.spacing = padding
stackView.axis = .vertical
return stackView
}()

private lazy var headerStackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.setContentHuggingPriority(.required, for: .vertical)
stackView.setContentCompressionResistancePriority(.required, for: .vertical)
stackView.distribution = .equalSpacing
stackView.alignment = .fill
stackView.axis = .horizontal
return stackView
}()

private lazy var imageAndTitleStackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.setContentHuggingPriority(.required, for: .vertical)
stackView.setContentCompressionResistancePriority(.required, for: .vertical)
stackView.distribution = .fill
stackView.spacing = basePadding
stackView.alignment = .fill
stackView.axis = .horizontal
return stackView
}()

private lazy var iconImageContainerView: UIView = {
let view = UIView()
view.setContentCompressionResistancePriority(.required, for: .vertical)
view.setContentHuggingPriority(.required, for: .vertical)
return view
}()

private lazy var iconImageView: UIImageView = {
let icon = WKSFSymbolIcon.for(symbol: .plusCircleFill) // temp waiting for design
let imageView = UIImageView(image: icon)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.setContentCompressionResistancePriority(.required, for: .vertical)
imageView.setContentHuggingPriority(.required, for: .vertical)
imageView.contentMode = .scaleAspectFit
return imageView
}()

private lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.setContentCompressionResistancePriority(.required, for: .vertical)
label.setContentHuggingPriority(.required, for: .vertical)
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
return label
}()

private lazy var textView: UITextView = {
let textfield = UITextView(frame: .zero)
textfield.translatesAutoresizingMaskIntoConstraints = false
textfield.layer.cornerRadius = 10
return textfield
}()

private lazy var placeholder: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()

lazy var nextButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()

private let basePadding: CGFloat = 8
private let padding: CGFloat = 16

// MARK: Lifecycle

public init(frame: CGRect, viewModel: AltTextExperimentModalSheetViewModel) {
self.viewModel = viewModel
super.init(frame: frame)
textView.delegate = self
setup()
}

public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: Methods

public override func appEnvironmentDidChange() {
super.appEnvironmentDidChange()
configure()
}

func updateColors() {
backgroundColor = theme.midBackground
titleLabel.textColor = theme.text
textView.backgroundColor = theme.paperBackground
iconImageView.tintColor = theme.link
nextButton.setTitleColor(theme.link, for: .normal)
nextButton.setTitleColor(theme.secondaryText, for: .disabled)
placeholder.textColor = theme.secondaryText
textView.textColor = theme.text
}

func configure() {
updateColors()
updateNextButtonState()
updatePlaceholderVisibility()

titleLabel.text = viewModel?.localizedStrings.title
nextButton.setTitle(viewModel?.localizedStrings.buttonTitle, for: .normal)
placeholder.text = viewModel?.localizedStrings.textViewPlaceholder

textView.font = WKFont.for(.callout, compatibleWith: traitCollection)

titleLabel.font = WKFont.for(.boldTitle3, compatibleWith: traitCollection)
nextButton.titleLabel?.font = WKFont.for(.semiboldHeadline, compatibleWith: traitCollection)
placeholder.font = WKFont.for(.callout, compatibleWith: traitCollection)
}

func setup() {
configure()

textView.addSubview(placeholder)
iconImageContainerView.addSubview(iconImageView)

imageAndTitleStackView.addArrangedSubview(iconImageContainerView)
imageAndTitleStackView.addArrangedSubview(titleLabel)

headerStackView.addArrangedSubview(imageAndTitleStackView)
headerStackView.addArrangedSubview(nextButton)

stackView.addArrangedSubview(headerStackView)
stackView.addArrangedSubview(textView)

scrollView.addSubview(stackView)
addSubview(scrollView)

NSLayoutConstraint.activate([

scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
scrollView.topAnchor.constraint(equalTo: topAnchor),
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),

stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: padding),
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -padding),
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: basePadding),
stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -padding),

textView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
textView.heightAnchor.constraint(equalToConstant: 125),

nextButton.heightAnchor.constraint(equalToConstant:44),

placeholder.topAnchor.constraint(equalTo: textView.topAnchor, constant: basePadding),
placeholder.leadingAnchor.constraint(equalTo: textView.leadingAnchor, constant: basePadding),


iconImageContainerView.centerXAnchor.constraint(equalTo: iconImageView.centerXAnchor),
iconImageView.leadingAnchor.constraint(equalTo: iconImageContainerView.leadingAnchor),
iconImageView.trailingAnchor.constraint(equalTo: iconImageContainerView.trailingAnchor),
iconImageView.topAnchor.constraint(equalTo: iconImageContainerView.topAnchor),
iconImageView.bottomAnchor.constraint(equalTo: iconImageContainerView.bottomAnchor),
iconImageContainerView.topAnchor.constraint(equalTo: titleLabel.topAnchor)
])
}

private func updateNextButtonState() {
nextButton.isEnabled = !textView.text.isEmpty
}

private func updatePlaceholderVisibility() {
placeholder.isHidden = !textView.text.isEmpty
}
}

extension AltTextExperimentModalSheetView: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
updateNextButtonState()
updatePlaceholderVisibility()
}

func textViewDidBeginEditing(_ textView: UITextView) {
placeholder.isHidden = true
}

func textViewDidEndEditing(_ textView: UITextView) {
updatePlaceholderVisibility()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import UIKit

final public class AltTextExperimentModalSheetViewController: WKCanvasViewController {

weak var viewModel: AltTextExperimentModalSheetViewModel?

public init(viewModel: AltTextExperimentModalSheetViewModel?) {
self.viewModel = viewModel
super.init()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let viewModel else { return }
let view = AltTextExperimentModalSheetView(frame: UIScreen.main.bounds, viewModel: viewModel)
addComponent(view, pinToEdges: true)
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation

@objc final public class AltTextExperimentModalSheetViewModel: NSObject {
public var altTextViewModel: AltTextExperimentViewModel
public var localizedStrings: LocalizedStrings

public struct LocalizedStrings {
public var title: String
public var buttonTitle: String
public var textViewPlaceholder: String

public init(title: String, buttonTitle: String, textViewPlaceholder: String) {
self.title = title
self.buttonTitle = buttonTitle
self.textViewPlaceholder = textViewPlaceholder
}
}

public init(altTextViewModel: AltTextExperimentViewModel, localizedStrings: LocalizedStrings) {
self.altTextViewModel = altTextViewModel
self.localizedStrings = localizedStrings
}

}
42 changes: 37 additions & 5 deletions Wikipedia/Code/ArticleViewController.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import UIKit
import Components
import WMF
import CocoaLumberjackSwift

Expand Down Expand Up @@ -90,8 +90,13 @@ class ArticleViewController: ViewController, HintPresenting {
SurveyAnnouncementsController.shared.activeSurveyAnnouncementResultForArticleURL(articleURL)
}
// END: Article As Living Doc properties

@objc init?(articleURL: URL, dataStore: MWKDataStore, theme: Theme, schemeHandler: SchemeHandler? = nil) {

// MARK: Alt-text experiment Properties

public var altTextBottomSheetViewModel: AltTextExperimentModalSheetViewModel?
private let needsAltTextExperimentSheet: Bool

@objc init?(articleURL: URL, dataStore: MWKDataStore, theme: Theme, schemeHandler: SchemeHandler? = nil, needsAltTextExperimentSheet: Bool = false, altTextBottomSheetViewModel: AltTextExperimentModalSheetViewModel? = nil) {
guard let article = dataStore.fetchOrCreateArticle(with: articleURL) else {
return nil
}
Expand All @@ -104,7 +109,9 @@ class ArticleViewController: ViewController, HintPresenting {
self.dataStore = dataStore
self.schemeHandler = schemeHandler ?? SchemeHandler(scheme: "app", session: dataStore.session)
self.cacheController = cacheController

self.needsAltTextExperimentSheet = needsAltTextExperimentSheet
self.altTextBottomSheetViewModel = altTextBottomSheetViewModel

super.init(theme: theme)

self.surveyTimerController = ArticleSurveyTimerController(delegate: self)
Expand All @@ -119,6 +126,7 @@ class ArticleViewController: ViewController, HintPresenting {
contentSizeObservation?.invalidate()
messagingController.removeScriptMessageHandler()
articleLoadWaitGroup = nil
altTextBottomSheetViewModel = nil
NotificationCenter.default.removeObserver(self)
}

Expand Down Expand Up @@ -362,8 +370,32 @@ class ArticleViewController: ViewController, HintPresenting {
}
showAnnouncementIfNeeded()
isFirstAppearance = false

presentAltTextExperimentSheet()

}


private func presentAltTextExperimentSheet() {
guard let altTextBottomSheetViewModel else { return }
let bottomSheetViewController = AltTextExperimentModalSheetViewController(viewModel: altTextBottomSheetViewModel)

if #available(iOS 16.0, *) {
if let sheet = bottomSheetViewController.sheetPresentationController {
let customSmallId = UISheetPresentationController.Detent.Identifier("customSmall")
let customSmallDetent = UISheetPresentationController.Detent.custom(identifier: customSmallId) { context in
return 44
}
sheet.detents = [customSmallDetent, .medium(), .large()]
sheet.selectedDetentIdentifier = .medium
sheet.largestUndimmedDetentIdentifier = .medium
sheet.prefersGrabberVisible = true
}
bottomSheetViewController.isModalInPresentation = true

present(bottomSheetViewController, animated: true, completion: nil)
}
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
tableOfContentsController.update(with: traitCollection)
Expand Down
17 changes: 7 additions & 10 deletions Wikipedia/Code/ExploreViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1286,19 +1286,11 @@ extension ExploreViewController: WKImageRecommendationsDelegate {
}

func imageRecommendationDidTriggerAltTextExperimentPanel(isFlowB: Bool, imageRecommendationsViewController: WKImageRecommendationsViewController) {

guard let viewModel = imageRecommendationsViewModel,
let lastRecommendation = viewModel.lastRecommendation else {
return
}

guard let viewModel = imageRecommendationsViewModel,
let lastRecommendation = viewModel.lastRecommendation else {
return
}

let altTextViewModel = AltTextExperimentViewModel(articleTitle: lastRecommendation.imageData.pageTitle, caption: lastRecommendation.caption, imageFullURL: lastRecommendation.imageData.fullUrl, imageThumbURL: lastRecommendation.imageData.thumbUrl, filename: lastRecommendation.imageData.displayFilename)

DispatchQueue.main.async {

let primaryTapHandler: ScrollableEducationPanelButtonTapHandler = { [weak self] _, _ in
Expand All @@ -1310,10 +1302,15 @@ extension ExploreViewController: WKImageRecommendationsDelegate {

let articleTitle = lastRecommendation.imageData.pageTitle
let altTextViewModel = AltTextExperimentViewModel(articleTitle: articleTitle, caption: lastRecommendation.caption, imageFullURL: lastRecommendation.imageData.fullUrl, imageThumbURL: lastRecommendation.imageData.thumbUrl, filename: lastRecommendation.imageData.displayFilename)
let addAltTextTitle = WMFLocalizedString("alt-text-experiment-view-title", value: "Add alt text", comment: "Title text for alt text experiment view")
let textViewPlaceholder = WMFLocalizedString("alt-text-experiment-text-field-placholder", value: "Describe the image", comment: "Text used for the text field placholder on the alt text view")
let localizedStrings = AltTextExperimentModalSheetViewModel.LocalizedStrings(title: addAltTextTitle, buttonTitle: CommonStrings.nextTitle, textViewPlaceholder: textViewPlaceholder)

let bottomSheetViewModel = AltTextExperimentModalSheetViewModel(altTextViewModel: altTextViewModel, localizedStrings: localizedStrings)

if let siteURL = viewModel.project.siteURL,
let articleURL = siteURL.wmf_URL(withTitle: articleTitle),
let articleViewController = ArticleViewController(articleURL: articleURL, dataStore: self.dataStore, theme: self.theme) {

let articleViewController = ArticleViewController(articleURL: articleURL, dataStore: self.dataStore, theme: self.theme, needsAltTextExperimentSheet: true, altTextBottomSheetViewModel: bottomSheetViewModel) {
self.navigationController?.pushViewController(articleViewController, animated: true)
}

Expand Down
2 changes: 1 addition & 1 deletion Wikipedia/Code/WMFAppViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -1360,7 +1360,7 @@ - (WMFArticleViewController *)showArticleWithURL:(NSURL *)articleURL animated:(B
[nc dismissViewControllerAnimated:NO completion:NULL];
}

WMFArticleViewController *articleVC = [[WMFArticleViewController alloc] initWithArticleURL:articleURL dataStore:self.dataStore theme:self.theme schemeHandler:nil];
WMFArticleViewController *articleVC = [[WMFArticleViewController alloc] initWithArticleURL:articleURL dataStore:self.dataStore theme:self.theme schemeHandler:nil needsAltTextExperimentSheet:NO altTextBottomSheetViewModel:nil];
articleVC.loadCompletion = completion;

#if DEBUG
Expand Down
2 changes: 1 addition & 1 deletion Wikipedia/Code/WMFFirstRandomViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ - (void)viewDidAppear:(BOOL)animated {
[[WMFAlertManager sharedInstance] showErrorAlert:error ?: [WMFFetcher unexpectedResponseError] sticky:NO dismissPreviousAlerts:NO tapCallBack:NULL];
return;
}
WMFRandomArticleViewController *randomArticleVC = [[WMFRandomArticleViewController alloc] initWithArticleURL:articleURL dataStore:self.dataStore theme:self.theme schemeHandler:nil];
WMFRandomArticleViewController *randomArticleVC = [[WMFRandomArticleViewController alloc] initWithArticleURL:articleURL dataStore:self.dataStore theme:self.theme schemeHandler:nil needsAltTextExperimentSheet:NO altTextBottomSheetViewModel:nil];
NSMutableArray *viewControllers = [self.navigationController.viewControllers mutableCopy];
[viewControllers replaceObjectAtIndex:viewControllers.count - 1 withObject:randomArticleVC];
[self.navigationController setViewControllers:viewControllers];
Expand Down
Loading

0 comments on commit 35d2ef3

Please sign in to comment.