Skip to content

Commit 66f4b14

Browse files
committed
Merge branch 'fix-file_length-warning-in-problemreportviewcontroller-ios-500'
2 parents 1ad14a0 + 387edd7 commit 66f4b14

File tree

5 files changed

+472
-429
lines changed

5 files changed

+472
-429
lines changed

ios/MullvadVPN.xcodeproj/project.pbxproj

+9-1
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,8 @@
648648
A988A3E22AFE54AC0008D2C7 /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */; };
649649
A988DF272ADE86ED00D807EF /* WireGuardObfuscationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */; };
650650
A988DF2A2ADE880300D807EF /* TunnelSettingsV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988DF282ADE880300D807EF /* TunnelSettingsV3.swift */; };
651+
A99E5EE02B7628150033F241 /* ProblemReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99E5EDF2B7628150033F241 /* ProblemReportViewModel.swift */; };
652+
A99E5EE22B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99E5EE12B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift */; };
651653
A9A1DE792AD5708E0073F689 /* TransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A1DE782AD5708E0073F689 /* TransportStrategy.swift */; };
652654
A9A5F9E12ACB05160083449F /* AddressCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114028F841390037AF9A /* AddressCacheTracker.swift */; };
653655
A9A5F9E22ACB05160083449F /* BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A0A2A338E4300100D75 /* BackgroundTask.swift */; };
@@ -1831,6 +1833,8 @@
18311833
A98502022B627B120061901E /* LocalNetworkProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNetworkProbe.swift; sourceTree = "<group>"; };
18321834
A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardObfuscationSettings.swift; sourceTree = "<group>"; };
18331835
A988DF282ADE880300D807EF /* TunnelSettingsV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV3.swift; sourceTree = "<group>"; };
1836+
A99E5EDF2B7628150033F241 /* ProblemReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportViewModel.swift; sourceTree = "<group>"; };
1837+
A99E5EE12B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProblemReportViewController+ViewManagement.swift"; sourceTree = "<group>"; };
18341838
A9A1DE782AD5708E0073F689 /* TransportStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransportStrategy.swift; sourceTree = "<group>"; };
18351839
A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerTests.swift; sourceTree = "<group>"; };
18361840
A9A8A8EA2A262AB30086D569 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; };
@@ -2311,10 +2315,12 @@
23112315
583FE01929C19760006E85F9 /* ProblemReport */ = {
23122316
isa = PBXGroup;
23132317
children = (
2318+
5878A26E2907E7E00096FC88 /* ProblemReportInteractor.swift */,
23142319
58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */,
23152320
58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */,
23162321
58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */,
2317-
5878A26E2907E7E00096FC88 /* ProblemReportInteractor.swift */,
2322+
A99E5EE12B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift */,
2323+
A99E5EDF2B7628150033F241 /* ProblemReportViewModel.swift */,
23182324
);
23192325
path = ProblemReport;
23202326
sourceTree = "<group>";
@@ -5098,9 +5104,11 @@
50985104
7A9CCCB62A96302800DD6A34 /* OutOfTimeCoordinator.swift in Sources */,
50995105
5827B0AA2B0F4C9100CCBBA1 /* EditAccessMethodViewControllerDelegate.swift in Sources */,
51005106
7A5869A82B5140C200640D27 /* MethodSettingsValidationErrorContentView.swift in Sources */,
5107+
A99E5EE22B762ED30033F241 /* ProblemReportViewController+ViewManagement.swift in Sources */,
51015108
7A5869A22B502EA800640D27 /* MethodSettingsSectionIdentifier.swift in Sources */,
51025109
586C0D812B03CA8400E7CDD7 /* CurrentValueSubject+UIActionBindings.swift in Sources */,
51035110
581DFAEA2B176C51005D6D1C /* PersistentProxyConfiguration+ViewModel.swift in Sources */,
5111+
A99E5EE02B7628150033F241 /* ProblemReportViewModel.swift in Sources */,
51045112
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */,
51055113
58B43C1925F77DB60002C8C3 /* TunnelControlView.swift in Sources */,
51065114
F09A297B2A9F8A9B00EA3B6F /* LogoutDialogueView.swift in Sources */,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
//
2+
// ProblemReportViewController+ViewManagement.swift
3+
// MullvadVPN
4+
//
5+
// Created by Marco Nikic on 2024-02-09.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import UIKit
11+
12+
extension ProblemReportViewController {
13+
func makeScrollView() -> UIScrollView {
14+
let scrollView = UIScrollView()
15+
scrollView.translatesAutoresizingMaskIntoConstraints = false
16+
scrollView.backgroundColor = .clear
17+
return scrollView
18+
}
19+
20+
func makeContainerView() -> UIView {
21+
let containerView = UIView()
22+
containerView.translatesAutoresizingMaskIntoConstraints = false
23+
containerView.directionalLayoutMargins = UIMetrics.contentLayoutMargins
24+
containerView.backgroundColor = .clear
25+
return containerView
26+
}
27+
28+
func makeSubheaderLabel() -> UILabel {
29+
let textLabel = UILabel()
30+
textLabel.translatesAutoresizingMaskIntoConstraints = false
31+
textLabel.numberOfLines = 0
32+
textLabel.textColor = .white
33+
textLabel.text = Self.persistentViewModel.subheadLabelText
34+
return textLabel
35+
}
36+
37+
func makeEmailTextField() -> CustomTextField {
38+
let textField = CustomTextField()
39+
textField.translatesAutoresizingMaskIntoConstraints = false
40+
textField.delegate = self
41+
textField.keyboardType = .emailAddress
42+
textField.textContentType = .emailAddress
43+
textField.autocorrectionType = .no
44+
textField.autocapitalizationType = .none
45+
textField.smartInsertDeleteType = .no
46+
textField.returnKeyType = .next
47+
textField.borderStyle = .none
48+
textField.backgroundColor = .white
49+
textField.inputAccessoryView = emailAccessoryToolbar
50+
textField.font = UIFont.systemFont(ofSize: 17)
51+
textField.placeholder = Self.persistentViewModel.emailPlaceholderText
52+
return textField
53+
}
54+
55+
func makeMessageTextView() -> CustomTextView {
56+
let textView = CustomTextView()
57+
textView.translatesAutoresizingMaskIntoConstraints = false
58+
textView.backgroundColor = .white
59+
textView.inputAccessoryView = messageAccessoryToolbar
60+
textView.font = UIFont.systemFont(ofSize: 17)
61+
textView.placeholder = Self.persistentViewModel.messageTextViewPlaceholder
62+
textView.contentInsetAdjustmentBehavior = .never
63+
64+
return textView
65+
}
66+
67+
func makeTextFieldsHolder() -> UIView {
68+
let view = UIView()
69+
view.translatesAutoresizingMaskIntoConstraints = false
70+
return view
71+
}
72+
73+
func makeMessagePlaceholderView() -> UIView {
74+
let view = UIView()
75+
view.translatesAutoresizingMaskIntoConstraints = false
76+
view.backgroundColor = .clear
77+
return view
78+
}
79+
80+
func makeButtonsStackView() -> UIStackView {
81+
let stackView = UIStackView(arrangedSubviews: [self.viewLogsButton, self.sendButton])
82+
stackView.translatesAutoresizingMaskIntoConstraints = false
83+
stackView.axis = .vertical
84+
stackView.spacing = 18
85+
86+
return stackView
87+
}
88+
89+
func makeViewLogsButton() -> AppButton {
90+
let button = AppButton(style: .default)
91+
button.translatesAutoresizingMaskIntoConstraints = false
92+
button.setTitle(Self.persistentViewModel.viewLogsButtonTitle, for: .normal)
93+
button.addTarget(self, action: #selector(handleViewLogsButtonTap), for: .touchUpInside)
94+
return button
95+
}
96+
97+
func makeSendButton() -> AppButton {
98+
let button = AppButton(style: .success)
99+
button.translatesAutoresizingMaskIntoConstraints = false
100+
button.setTitle(Self.persistentViewModel.sendLogsButtonTitle, for: .normal)
101+
button.addTarget(self, action: #selector(handleSendButtonTap), for: .touchUpInside)
102+
return button
103+
}
104+
105+
func makeSubmissionOverlayView() -> ProblemReportSubmissionOverlayView {
106+
let overlay = ProblemReportSubmissionOverlayView()
107+
overlay.translatesAutoresizingMaskIntoConstraints = false
108+
109+
overlay.editButtonAction = { [weak self] in
110+
self?.hideSubmissionOverlay()
111+
}
112+
113+
overlay.retryButtonAction = { [weak self] in
114+
self?.sendProblemReport()
115+
}
116+
117+
return overlay
118+
}
119+
120+
func addConstraints() {
121+
activeMessageTextViewConstraints =
122+
messageTextView.pinEdges(.all().excluding(.top), to: view) +
123+
messageTextView.pinEdges(PinnableEdges([.top(0)]), to: view.safeAreaLayoutGuide)
124+
125+
inactiveMessageTextViewConstraints =
126+
messageTextView.pinEdges(.all().excluding(.top), to: textFieldsHolder) +
127+
[messageTextView.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 12)]
128+
129+
textFieldsHolder.addSubview(emailTextField)
130+
textFieldsHolder.addSubview(messagePlaceholder)
131+
textFieldsHolder.addSubview(messageTextView)
132+
133+
scrollView.addSubview(containerView)
134+
containerView.addSubview(subheaderLabel)
135+
containerView.addSubview(textFieldsHolder)
136+
containerView.addSubview(buttonsStackView)
137+
138+
view.addConstrainedSubviews([scrollView]) {
139+
inactiveMessageTextViewConstraints
140+
141+
subheaderLabel.pinEdges(.all().excluding(.bottom), to: containerView.layoutMarginsGuide)
142+
143+
textFieldsHolder.pinEdges(PinnableEdges([.leading(0), .trailing(0)]), to: containerView.layoutMarginsGuide)
144+
textFieldsHolder.topAnchor.constraint(equalTo: subheaderLabel.bottomAnchor, constant: 24)
145+
146+
buttonsStackView.pinEdges(.all().excluding(.top), to: containerView.layoutMarginsGuide)
147+
buttonsStackView.topAnchor.constraint(equalTo: textFieldsHolder.bottomAnchor, constant: 18)
148+
149+
emailTextField.pinEdges(.all().excluding(.bottom), to: textFieldsHolder)
150+
151+
messagePlaceholder.pinEdges(.all().excluding(.top), to: textFieldsHolder)
152+
messagePlaceholder.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 12)
153+
messagePlaceholder.heightAnchor.constraint(equalTo: messageTextView.heightAnchor)
154+
155+
scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor)
156+
scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor)
157+
scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor)
158+
scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor)
159+
160+
scrollView.contentLayoutGuide.topAnchor.constraint(equalTo: containerView.topAnchor)
161+
scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
162+
scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: containerView.leadingAnchor)
163+
scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor)
164+
scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor)
165+
scrollView.contentLayoutGuide.heightAnchor
166+
.constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.heightAnchor)
167+
168+
messageTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 150)
169+
}
170+
}
171+
172+
override func viewSafeAreaInsetsDidChange() {
173+
super.viewSafeAreaInsetsDidChange()
174+
175+
scrollViewKeyboardResponder?.updateContentInsets()
176+
textViewKeyboardResponder?.updateContentInsets()
177+
}
178+
179+
func makeKeyboardToolbar(canGoBackward: Bool, canGoForward: Bool) -> UIToolbar {
180+
var toolbarItems = UIBarButtonItem.makeKeyboardNavigationItems { prevButton, nextButton in
181+
prevButton.target = self
182+
prevButton.action = #selector(focusEmailTextField)
183+
prevButton.isEnabled = canGoBackward
184+
185+
nextButton.target = self
186+
nextButton.action = #selector(focusDescriptionTextView)
187+
nextButton.isEnabled = canGoForward
188+
}
189+
190+
toolbarItems.append(contentsOf: [
191+
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
192+
UIBarButtonItem(
193+
barButtonSystemItem: .done,
194+
target: self,
195+
action: #selector(dismissKeyboard)
196+
),
197+
])
198+
199+
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 100, height: 44))
200+
toolbar.items = toolbarItems
201+
return toolbar
202+
}
203+
204+
func setDescriptionFieldExpanded(_ isExpanded: Bool) {
205+
// Make voice over ignore siblings when expanded
206+
messageTextView.accessibilityViewIsModal = isExpanded
207+
208+
if isExpanded {
209+
// Disable the large title
210+
navigationItem.largeTitleDisplayMode = .never
211+
212+
// Move the text view above scroll view
213+
view.addSubview(messageTextView)
214+
215+
// Re-add old constraints
216+
NSLayoutConstraint.activate(inactiveMessageTextViewConstraints)
217+
218+
// Do a layout pass
219+
view.layoutIfNeeded()
220+
221+
// Swap constraints
222+
NSLayoutConstraint.deactivate(inactiveMessageTextViewConstraints)
223+
NSLayoutConstraint.activate(activeMessageTextViewConstraints)
224+
225+
// Enable content inset adjustment on text view
226+
messageTextView.contentInsetAdjustmentBehavior = .always
227+
228+
// Animate constraints & rounded corners on the text view
229+
animateDescriptionTextView(animations: {
230+
// Turn off rounded corners as the text view fills in the entire view
231+
self.messageTextView.roundCorners = false
232+
233+
self.view.layoutIfNeeded()
234+
}, completion: { _ in
235+
self.isMessageTextViewExpanded = true
236+
237+
self.textViewKeyboardResponder?.updateContentInsets()
238+
239+
// Tell accessibility engine to scan the new layout
240+
UIAccessibility.post(notification: .layoutChanged, argument: nil)
241+
})
242+
243+
} else {
244+
// Re-enable the large title
245+
navigationItem.largeTitleDisplayMode = .automatic
246+
247+
// Swap constraints
248+
NSLayoutConstraint.deactivate(activeMessageTextViewConstraints)
249+
NSLayoutConstraint.activate(inactiveMessageTextViewConstraints)
250+
251+
// Animate constraints & rounded corners on the text view
252+
animateDescriptionTextView(animations: {
253+
// Turn on rounded corners as the text view returns back to where it was
254+
self.messageTextView.roundCorners = true
255+
256+
self.view.layoutIfNeeded()
257+
}, completion: { _ in
258+
// Revert the content adjustment behavior
259+
self.messageTextView.contentInsetAdjustmentBehavior = .never
260+
261+
// Add the text view inside of the scroll view
262+
self.textFieldsHolder.addSubview(self.messageTextView)
263+
264+
self.isMessageTextViewExpanded = false
265+
266+
// Tell accessibility engine to scan the new layout
267+
UIAccessibility.post(notification: .layoutChanged, argument: nil)
268+
})
269+
}
270+
}
271+
272+
func animateDescriptionTextView(
273+
animations: @escaping () -> Void,
274+
completion: @escaping (Bool) -> Void
275+
) {
276+
UIView.animate(withDuration: 0.25, animations: animations) { completed in
277+
completion(completed)
278+
}
279+
}
280+
281+
func showSubmissionOverlay() {
282+
guard !showsSubmissionOverlay else { return }
283+
284+
showsSubmissionOverlay = true
285+
286+
view.addSubview(submissionOverlayView)
287+
288+
NSLayoutConstraint.activate([
289+
submissionOverlayView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
290+
submissionOverlayView.leadingAnchor
291+
.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
292+
submissionOverlayView.trailingAnchor
293+
.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
294+
submissionOverlayView.bottomAnchor
295+
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
296+
])
297+
298+
UIView.transition(
299+
from: scrollView,
300+
to: submissionOverlayView,
301+
duration: 0.25,
302+
options: [.showHideTransitionViews, .transitionCrossDissolve]
303+
) { _ in
304+
// success
305+
}
306+
}
307+
308+
func hideSubmissionOverlay() {
309+
guard showsSubmissionOverlay else { return }
310+
311+
showsSubmissionOverlay = false
312+
313+
UIView.transition(
314+
from: submissionOverlayView,
315+
to: scrollView,
316+
duration: 0.25,
317+
options: [.showHideTransitionViews, .transitionCrossDissolve]
318+
) { _ in
319+
// success
320+
self.submissionOverlayView.removeFromSuperview()
321+
}
322+
}
323+
}

0 commit comments

Comments
 (0)