Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[NTV-613] Change Email View SwiftUI (#1803)
* semi-built rendering of change email view * removed playground, will move swiftui code to ChangeEmailView * base view in SwiftUI for ChangeEmailViewController Still missing navigation bar content, but it is launching inside UIHostingViewController, so that's good news! * Adjusted VStack to have zero vertical spacing so seperator gets anchored to the bottom of the vstack. * styling applied to email, new email and password fields. * cleaned up modifier logic based on field type using cutom modifiers. * corrected return/done button logic, prevent submission until character limit on password is reached. * Added warning label * warningMessage updated for unverified email and email undeliverable. * added resend verification button and fixed spacing for button and warning label. * attempting to add message banner view controller...still tinkering with uiviewcontrollerrepresentable * separated some view modifier code that can be considered common. * wrote the uiviewcontrollerrepresentable and wrapper view. Needs a bit of tinkering to reconfigure the ChangelEmailView to contain an instance of MessageBannerView. * message banner view displaying. Some UI corrections required. * Recreated a non-animated MessageBannerView. A few things still needs mapping: - voice over and message banner view hide, banner message accessibility label. Also add animation and size change on orientation change. Also double check dynamic type and voice over when complete. * minor accessor scope tweak * completed message banner view * connecting the view model to the view is not quite working. Will have to use Combine instead of ReactiveSwift so its more complicated that first planned. * found a way to integrate existing view model into change email view. * completed functionality up to saving new email successfully TODO: 1. Ensure when save is complete, we update the save button. 2. Ensure both textfields are disabled while saving (ie. loading state == true) 3. After sucessful save then clear the new email and password fields * completing a save and an errored save are working but subsequent saves/errors don't show banner message. - subsequent saves not working because save is not enabled - subsequent saves not showing erorr/success banner message. - validate that verification email view is hidden after email is verified. - fix backspace issue triggering save. * added subsequent saves and banner show * adjusted the banner to be a little more space conscious with the test. Banner view looks a bit over spaced when on larger screens and fits most text in when smaller screens go landscape. This can be tightened in the future. * deleted some unused files and fixed a production bug related to unseen current email. * corrected message text
- Loading branch information
Showing
9 changed files
with
752 additions
and
1 deletion.
There are no files selected for viewing
235 changes: 235 additions & 0 deletions
235
Kickstarter-iOS/Features/ChangeEmail/Controller/ChangeEmailView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
import Combine | ||
import Library | ||
import SwiftUI | ||
|
||
enum FocusField { | ||
case newEmail | ||
case currentPassword | ||
} | ||
|
||
@available(iOS 15.0, *) | ||
struct ChangeEmailView: View { | ||
@SwiftUI.Environment(\.defaultMinListRowHeight) var minListRow | ||
@FocusState private var focusField: FocusField? | ||
private let contentPadding = 12.0 | ||
@ObservedObject private var reactiveViewModel = ChangeEmailViewModel_SwiftUIIntegrationTest() | ||
@State private var retrievedEmailText = "" | ||
@State private var newEmailText = "" | ||
@State private var newPasswordText = "" | ||
@State private var saveEnabled = false | ||
@State private var saveTriggered = false | ||
@State private var showLoading = false | ||
@State private var showBannerMessage = false | ||
@State private var bannerMessage: MessageBannerViewViewModel? | ||
|
||
var body: some View { | ||
GeometryReader { proxy in | ||
List { | ||
Color(.ksr_support_100) | ||
.frame(maxWidth: .infinity, maxHeight: minListRow, alignment: .center) | ||
.listRowSeparator(.hidden) | ||
.listRowInsets(EdgeInsets()) | ||
|
||
VStack(alignment: .center, spacing: 0) { | ||
InputFieldView( | ||
titleText: Strings.Current_email(), | ||
secureField: false, | ||
placeholderText: "", | ||
contentPadding: contentPadding, | ||
valueText: $retrievedEmailText | ||
) | ||
.currentEmail() | ||
.onReceive(reactiveViewModel.retrievedEmailText) { newValue in | ||
retrievedEmailText = newValue | ||
} | ||
|
||
Color(.ksr_cell_separator).frame(maxWidth: .infinity, maxHeight: 1) | ||
} | ||
.listRowSeparator(.hidden) | ||
.listRowInsets(EdgeInsets()) | ||
|
||
if !reactiveViewModel.hideMessageLabel { | ||
warningLabel( | ||
text: reactiveViewModel.warningMessageWithAlert.0, | ||
reactiveViewModel.warningMessageWithAlert.1 | ||
) | ||
.frame(maxWidth: .infinity, maxHeight: minListRow, alignment: .leading) | ||
.background(Color(.ksr_support_100)) | ||
.listRowSeparator(.hidden) | ||
.listRowInsets(EdgeInsets()) | ||
} | ||
|
||
if !reactiveViewModel.hideVerifyView { | ||
VStack(alignment: .leading, spacing: 0) { | ||
Button(reactiveViewModel.verifyEmailButtonTitle) { | ||
reactiveViewModel.inputs.resendVerificationEmailButtonTapped() | ||
} | ||
.font(Font(UIFont.ksr_body())) | ||
.foregroundColor(Color(.ksr_create_700)) | ||
.padding(contentPadding) | ||
.disabled(showLoading) | ||
|
||
Color(.ksr_cell_separator).frame(maxWidth: .infinity, maxHeight: 1) | ||
} | ||
.listRowSeparator(.hidden) | ||
.listRowInsets(EdgeInsets()) | ||
} | ||
|
||
Color(.ksr_support_100) | ||
.frame(maxWidth: .infinity, maxHeight: minListRow, alignment: .center) | ||
.listRowSeparator(.hidden) | ||
.listRowInsets(EdgeInsets()) | ||
|
||
VStack(alignment: .center, spacing: 0) { | ||
InputFieldView( | ||
titleText: Strings.New_email(), | ||
secureField: false, | ||
placeholderText: Strings.login_placeholder_email(), | ||
contentPadding: contentPadding, | ||
valueText: $newEmailText | ||
) | ||
.onReceive(reactiveViewModel.resetEditableText) { newValue in | ||
if newValue { | ||
newEmailText = "" | ||
} | ||
} | ||
.onChange(of: newEmailText) { newValue in | ||
reactiveViewModel.newEmailText.send(newValue) | ||
} | ||
.newEmail(editable: !showLoading) | ||
.focused($focusField, equals: .newEmail) | ||
.onSubmit { | ||
focusField = .currentPassword | ||
} | ||
|
||
InputFieldView( | ||
titleText: Strings.Current_password(), | ||
secureField: true, | ||
placeholderText: Strings.login_placeholder_password(), | ||
contentPadding: contentPadding, | ||
valueText: $newPasswordText | ||
) | ||
.onChange(of: newPasswordText) { newValue in | ||
reactiveViewModel.currentPasswordText.send(newValue) | ||
} | ||
.currentPassword(editable: !showLoading) | ||
.focused($focusField, equals: .currentPassword) | ||
.onReceive(reactiveViewModel.resetEditableText) { resetFlag in | ||
if resetFlag { | ||
newPasswordText = "" | ||
} | ||
} | ||
// FIXME: So "Done" on keyboard doesn't trigger Save --> in the future we might want to add this (was in the old `ChangeEmailViewController`) | ||
|
||
Color(.ksr_cell_separator).frame(maxWidth: .infinity, maxHeight: 1) | ||
} | ||
.listRowSeparator(.hidden) | ||
.listRowInsets(EdgeInsets()) | ||
} | ||
.navigationTitle(Strings.Change_email()) | ||
.background(Color(.ksr_support_100)) | ||
.listStyle(.plain) | ||
.toolbar { | ||
ToolbarItem(placement: .navigationBarTrailing) { | ||
LoadingBarButtonItem( | ||
saveEnabled: $saveEnabled, | ||
saveTriggered: $saveTriggered, | ||
showLoading: $showLoading, | ||
titleText: Strings.Save() | ||
) | ||
.onReceive(reactiveViewModel.saveButtonEnabled) { newValue in | ||
saveEnabled = newValue | ||
} | ||
.onReceive(reactiveViewModel.resetEditableText) { newValue in | ||
showLoading = !newValue | ||
} | ||
.onChange(of: saveTriggered) { newValue in | ||
focusField = nil | ||
reactiveViewModel.saveTriggered.send(newValue) | ||
} | ||
} | ||
} | ||
.overlay(alignment: .bottom) { | ||
MessageBannerView(viewModel: $bannerMessage) | ||
.frame( | ||
minWidth: proxy.size.width, | ||
idealWidth: proxy.size.width, | ||
maxHeight: proxy.size.height / 5, | ||
alignment: .bottom | ||
) | ||
.animation(.easeInOut) | ||
} | ||
.onReceive(reactiveViewModel.bannerMessage) { newValue in | ||
bannerMessage = newValue | ||
} | ||
.onAppear { | ||
reactiveViewModel.inputs.viewDidLoad() | ||
} | ||
} | ||
} | ||
|
||
private struct InputFieldView: View { | ||
var titleText: String | ||
var secureField: Bool | ||
var placeholderText: String | ||
var contentPadding: CGFloat | ||
var valueText: Binding<String> | ||
|
||
var body: some View { | ||
HStack { | ||
Text(titleText) | ||
.frame( | ||
maxWidth: .infinity, | ||
alignment: .leading | ||
) | ||
.font(Font(UIFont.ksr_body())) | ||
.foregroundColor(Color(.ksr_support_700)) | ||
Spacer() | ||
|
||
InputFieldUserInputView( | ||
secureField: secureField, | ||
placeholderText: placeholderText, | ||
valueText: valueText | ||
) | ||
} | ||
.padding(contentPadding) | ||
.accessibilityElement(children: .combine) | ||
.accessibilityLabel(titleText) | ||
} | ||
} | ||
|
||
private struct InputFieldUserInputView: View { | ||
var secureField: Bool | ||
var placeholderText: String | ||
var valueText: Binding<String> | ||
|
||
var body: some View { | ||
if secureField { | ||
SecureField( | ||
"", | ||
text: valueText, | ||
prompt: Text(placeholderText).foregroundColor(Color(.ksr_support_400)) | ||
) | ||
} else { | ||
TextField( | ||
"", | ||
text: valueText, | ||
prompt: | ||
Text(placeholderText).foregroundColor(Color(.ksr_support_400)) | ||
) | ||
} | ||
} | ||
} | ||
|
||
@ViewBuilder | ||
private func warningLabel(text: String, _ alert: Bool) -> some View { | ||
let textColor = alert ? Color(.ksr_alert) : Color(.ksr_support_400) | ||
|
||
Label(text, systemImage: "exclamationmark.triangle.fill") | ||
.labelStyle(.titleOnly) | ||
.font(Font(UIFont.ksr_body(size: 13))) | ||
.lineLimit(nil) | ||
.padding([.leading, .trailing], self.contentPadding) | ||
.foregroundColor(textColor) | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
Kickstarter-iOS/Features/MessageBanner/Views/MessageBannerView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import Library | ||
import SwiftUI | ||
|
||
@available(iOS 15.0, *) | ||
struct MessageBannerView: View { | ||
@Binding var viewModel: MessageBannerViewViewModel? | ||
|
||
var body: some View { | ||
if let vm = viewModel { | ||
ZStack { | ||
RoundedRectangle(cornerRadius: 4) | ||
.foregroundColor(vm.bannerBackgroundColor) | ||
Label(vm.bannerMessage, image: vm.iconImageName) | ||
.font(Font(UIFont.ksr_subhead())) | ||
.foregroundColor(vm.messageTextColor) | ||
.lineLimit(3) | ||
.multilineTextAlignment(vm.messageTextAlignment) | ||
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10)) | ||
} | ||
.accessibilityElement() | ||
.accessibilityLabel(vm.bannerMessageAccessibilityLabel) | ||
.padding() | ||
.onAppear { | ||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { | ||
viewModel = nil | ||
} | ||
} | ||
.onTapGesture { | ||
viewModel = nil | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import Library | ||
import SwiftUI | ||
|
||
struct LoadingBarButtonItem: View { | ||
@Binding var saveEnabled: Bool | ||
@Binding var saveTriggered: Bool | ||
@Binding var showLoading: Bool | ||
@State var titleText: String | ||
|
||
var body: some View { | ||
let buttonColor = $saveEnabled.wrappedValue ? Color(.ksr_create_700) : Color(.ksr_create_300) | ||
|
||
HStack { | ||
if !showLoading { | ||
Button(titleText) { | ||
showLoading = true | ||
saveTriggered = true | ||
} | ||
.font(Font(UIFont.systemFont(ofSize: 17))) | ||
.foregroundColor(buttonColor) | ||
.disabled(!$saveEnabled.wrappedValue) | ||
} else { | ||
ProgressView() | ||
.foregroundColor(Color(.ksr_support_700)) | ||
.onDisappear { | ||
saveTriggered = false | ||
} | ||
} | ||
} | ||
.accessibilityElement(children: .combine) | ||
.accessibilityLabel(titleText) | ||
} | ||
} |
Oops, something went wrong.