Skip to content

Commit

Permalink
Merge branch 'fix-file_length-warning-in-problemreportviewcontroller-…
Browse files Browse the repository at this point in the history
…ios-500'
  • Loading branch information
buggmagnet committed Feb 12, 2024
2 parents 1ad14a0 + 387edd7 commit 66f4b14
Show file tree
Hide file tree
Showing 5 changed files with 472 additions and 429 deletions.
10 changes: 9 additions & 1 deletion ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,8 @@
A988A3E22AFE54AC0008D2C7 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; };
A988DF272ADE86ED00D807EF /* WireGuardObfuscationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */; };
A988DF2A2ADE880300D807EF /* TunnelSettingsV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988DF282ADE880300D807EF /* TunnelSettingsV3.swift */; };
A99E5EE02B7628150033F241 /* ProblemReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99E5EDF2B7628150033F241 /* ProblemReportViewModel.swift */; };
A99E5EE22B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99E5EE12B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift */; };
A9A1DE792AD5708E0073F689 /* TransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A1DE782AD5708E0073F689 /* TransportStrategy.swift */; };
A9A5F9E12ACB05160083449F /* AddressCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114028F841390037AF9A /* AddressCacheTracker.swift */; };
A9A5F9E22ACB05160083449F /* BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A0A2A338E4300100D75 /* BackgroundTask.swift */; };
Expand Down Expand Up @@ -1831,6 +1833,8 @@
A98502022B627B120061901E /* LocalNetworkProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNetworkProbe.swift; sourceTree = "<group>"; };
A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardObfuscationSettings.swift; sourceTree = "<group>"; };
A988DF282ADE880300D807EF /* TunnelSettingsV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV3.swift; sourceTree = "<group>"; };
A99E5EDF2B7628150033F241 /* ProblemReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportViewModel.swift; sourceTree = "<group>"; };
A99E5EE12B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProblemReportViewController+ViewManagement.swift"; sourceTree = "<group>"; };
A9A1DE782AD5708E0073F689 /* TransportStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransportStrategy.swift; sourceTree = "<group>"; };
A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerTests.swift; sourceTree = "<group>"; };
A9A8A8EA2A262AB30086D569 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2311,10 +2315,12 @@
583FE01929C19760006E85F9 /* ProblemReport */ = {
isa = PBXGroup;
children = (
5878A26E2907E7E00096FC88 /* ProblemReportInteractor.swift */,
58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */,
58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */,
58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */,
5878A26E2907E7E00096FC88 /* ProblemReportInteractor.swift */,
A99E5EE12B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift */,
A99E5EDF2B7628150033F241 /* ProblemReportViewModel.swift */,
);
path = ProblemReport;
sourceTree = "<group>";
Expand Down Expand Up @@ -5098,9 +5104,11 @@
7A9CCCB62A96302800DD6A34 /* OutOfTimeCoordinator.swift in Sources */,
5827B0AA2B0F4C9100CCBBA1 /* EditAccessMethodViewControllerDelegate.swift in Sources */,
7A5869A82B5140C200640D27 /* MethodSettingsValidationErrorContentView.swift in Sources */,
A99E5EE22B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift in Sources */,
7A5869A22B502EA800640D27 /* MethodSettingsSectionIdentifier.swift in Sources */,
586C0D812B03CA8400E7CDD7 /* CurrentValueSubject+UIActionBindings.swift in Sources */,
581DFAEA2B176C51005D6D1C /* PersistentProxyConfiguration+ViewModel.swift in Sources */,
A99E5EE02B7628150033F241 /* ProblemReportViewModel.swift in Sources */,
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */,
58B43C1925F77DB60002C8C3 /* TunnelControlView.swift in Sources */,
F09A297B2A9F8A9B00EA3B6F /* LogoutDialogueView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
//
// ProblemReportViewController+ViewManagement.swift
// MullvadVPN
//
// Created by Marco Nikic on 2024-02-09.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import Foundation
import UIKit

extension ProblemReportViewController {
func makeScrollView() -> UIScrollView {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.backgroundColor = .clear
return scrollView
}

func makeContainerView() -> UIView {
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.directionalLayoutMargins = UIMetrics.contentLayoutMargins
containerView.backgroundColor = .clear
return containerView
}

func makeSubheaderLabel() -> UILabel {
let textLabel = UILabel()
textLabel.translatesAutoresizingMaskIntoConstraints = false
textLabel.numberOfLines = 0
textLabel.textColor = .white
textLabel.text = Self.persistentViewModel.subheadLabelText
return textLabel
}

func makeEmailTextField() -> CustomTextField {
let textField = CustomTextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.delegate = self
textField.keyboardType = .emailAddress
textField.textContentType = .emailAddress
textField.autocorrectionType = .no
textField.autocapitalizationType = .none
textField.smartInsertDeleteType = .no
textField.returnKeyType = .next
textField.borderStyle = .none
textField.backgroundColor = .white
textField.inputAccessoryView = emailAccessoryToolbar
textField.font = UIFont.systemFont(ofSize: 17)
textField.placeholder = Self.persistentViewModel.emailPlaceholderText
return textField
}

func makeMessageTextView() -> CustomTextView {
let textView = CustomTextView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.backgroundColor = .white
textView.inputAccessoryView = messageAccessoryToolbar
textView.font = UIFont.systemFont(ofSize: 17)
textView.placeholder = Self.persistentViewModel.messageTextViewPlaceholder
textView.contentInsetAdjustmentBehavior = .never

return textView
}

func makeTextFieldsHolder() -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}

func makeMessagePlaceholderView() -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .clear
return view
}

func makeButtonsStackView() -> UIStackView {
let stackView = UIStackView(arrangedSubviews: [self.viewLogsButton, self.sendButton])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 18

return stackView
}

func makeViewLogsButton() -> AppButton {
let button = AppButton(style: .default)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle(Self.persistentViewModel.viewLogsButtonTitle, for: .normal)
button.addTarget(self, action: #selector(handleViewLogsButtonTap), for: .touchUpInside)
return button
}

func makeSendButton() -> AppButton {
let button = AppButton(style: .success)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle(Self.persistentViewModel.sendLogsButtonTitle, for: .normal)
button.addTarget(self, action: #selector(handleSendButtonTap), for: .touchUpInside)
return button
}

func makeSubmissionOverlayView() -> ProblemReportSubmissionOverlayView {
let overlay = ProblemReportSubmissionOverlayView()
overlay.translatesAutoresizingMaskIntoConstraints = false

overlay.editButtonAction = { [weak self] in
self?.hideSubmissionOverlay()
}

overlay.retryButtonAction = { [weak self] in
self?.sendProblemReport()
}

return overlay
}

func addConstraints() {
activeMessageTextViewConstraints =
messageTextView.pinEdges(.all().excluding(.top), to: view) +
messageTextView.pinEdges(PinnableEdges([.top(0)]), to: view.safeAreaLayoutGuide)

inactiveMessageTextViewConstraints =
messageTextView.pinEdges(.all().excluding(.top), to: textFieldsHolder) +
[messageTextView.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 12)]

textFieldsHolder.addSubview(emailTextField)
textFieldsHolder.addSubview(messagePlaceholder)
textFieldsHolder.addSubview(messageTextView)

scrollView.addSubview(containerView)
containerView.addSubview(subheaderLabel)
containerView.addSubview(textFieldsHolder)
containerView.addSubview(buttonsStackView)

view.addConstrainedSubviews([scrollView]) {
inactiveMessageTextViewConstraints

subheaderLabel.pinEdges(.all().excluding(.bottom), to: containerView.layoutMarginsGuide)

textFieldsHolder.pinEdges(PinnableEdges([.leading(0), .trailing(0)]), to: containerView.layoutMarginsGuide)
textFieldsHolder.topAnchor.constraint(equalTo: subheaderLabel.bottomAnchor, constant: 24)

buttonsStackView.pinEdges(.all().excluding(.top), to: containerView.layoutMarginsGuide)
buttonsStackView.topAnchor.constraint(equalTo: textFieldsHolder.bottomAnchor, constant: 18)

emailTextField.pinEdges(.all().excluding(.bottom), to: textFieldsHolder)

messagePlaceholder.pinEdges(.all().excluding(.top), to: textFieldsHolder)
messagePlaceholder.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 12)
messagePlaceholder.heightAnchor.constraint(equalTo: messageTextView.heightAnchor)

scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor)
scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor)
scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor)
scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor)

scrollView.contentLayoutGuide.topAnchor.constraint(equalTo: containerView.topAnchor)
scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: containerView.leadingAnchor)
scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor)
scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor)
scrollView.contentLayoutGuide.heightAnchor
.constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.heightAnchor)

messageTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 150)
}
}

override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()

scrollViewKeyboardResponder?.updateContentInsets()
textViewKeyboardResponder?.updateContentInsets()
}

func makeKeyboardToolbar(canGoBackward: Bool, canGoForward: Bool) -> UIToolbar {
var toolbarItems = UIBarButtonItem.makeKeyboardNavigationItems { prevButton, nextButton in
prevButton.target = self
prevButton.action = #selector(focusEmailTextField)
prevButton.isEnabled = canGoBackward

nextButton.target = self
nextButton.action = #selector(focusDescriptionTextView)
nextButton.isEnabled = canGoForward
}

toolbarItems.append(contentsOf: [
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
UIBarButtonItem(
barButtonSystemItem: .done,
target: self,
action: #selector(dismissKeyboard)
),
])

let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 100, height: 44))
toolbar.items = toolbarItems
return toolbar
}

func setDescriptionFieldExpanded(_ isExpanded: Bool) {
// Make voice over ignore siblings when expanded
messageTextView.accessibilityViewIsModal = isExpanded

if isExpanded {
// Disable the large title
navigationItem.largeTitleDisplayMode = .never

// Move the text view above scroll view
view.addSubview(messageTextView)

// Re-add old constraints
NSLayoutConstraint.activate(inactiveMessageTextViewConstraints)

// Do a layout pass
view.layoutIfNeeded()

// Swap constraints
NSLayoutConstraint.deactivate(inactiveMessageTextViewConstraints)
NSLayoutConstraint.activate(activeMessageTextViewConstraints)

// Enable content inset adjustment on text view
messageTextView.contentInsetAdjustmentBehavior = .always

// Animate constraints & rounded corners on the text view
animateDescriptionTextView(animations: {
// Turn off rounded corners as the text view fills in the entire view
self.messageTextView.roundCorners = false

self.view.layoutIfNeeded()
}, completion: { _ in
self.isMessageTextViewExpanded = true

self.textViewKeyboardResponder?.updateContentInsets()

// Tell accessibility engine to scan the new layout
UIAccessibility.post(notification: .layoutChanged, argument: nil)
})

} else {
// Re-enable the large title
navigationItem.largeTitleDisplayMode = .automatic

// Swap constraints
NSLayoutConstraint.deactivate(activeMessageTextViewConstraints)
NSLayoutConstraint.activate(inactiveMessageTextViewConstraints)

// Animate constraints & rounded corners on the text view
animateDescriptionTextView(animations: {
// Turn on rounded corners as the text view returns back to where it was
self.messageTextView.roundCorners = true

self.view.layoutIfNeeded()
}, completion: { _ in
// Revert the content adjustment behavior
self.messageTextView.contentInsetAdjustmentBehavior = .never

// Add the text view inside of the scroll view
self.textFieldsHolder.addSubview(self.messageTextView)

self.isMessageTextViewExpanded = false

// Tell accessibility engine to scan the new layout
UIAccessibility.post(notification: .layoutChanged, argument: nil)
})
}
}

func animateDescriptionTextView(
animations: @escaping () -> Void,
completion: @escaping (Bool) -> Void
) {
UIView.animate(withDuration: 0.25, animations: animations) { completed in
completion(completed)
}
}

func showSubmissionOverlay() {
guard !showsSubmissionOverlay else { return }

showsSubmissionOverlay = true

view.addSubview(submissionOverlayView)

NSLayoutConstraint.activate([
submissionOverlayView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
submissionOverlayView.leadingAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
submissionOverlayView.trailingAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
submissionOverlayView.bottomAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])

UIView.transition(
from: scrollView,
to: submissionOverlayView,
duration: 0.25,
options: [.showHideTransitionViews, .transitionCrossDissolve]
) { _ in
// success
}
}

func hideSubmissionOverlay() {
guard showsSubmissionOverlay else { return }

showsSubmissionOverlay = false

UIView.transition(
from: submissionOverlayView,
to: scrollView,
duration: 0.25,
options: [.showHideTransitionViews, .transitionCrossDissolve]
) { _ in
// success
self.submissionOverlayView.removeFromSuperview()
}
}
}
Loading

0 comments on commit 66f4b14

Please sign in to comment.