diff --git a/BeeSwift.xcodeproj/project.pbxproj b/BeeSwift.xcodeproj/project.pbxproj index 37044a77..5448af84 100644 --- a/BeeSwift.xcodeproj/project.pbxproj +++ b/BeeSwift.xcodeproj/project.pbxproj @@ -7,14 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + 9B2488422D030E5100E610E0 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2488412D030E5100E610E0 /* SignInView.swift */; }; 9B65F2322CFA6427009674A7 /* DeeplinkGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B65F2312CFA6418009674A7 /* DeeplinkGenerator.swift */; }; + 9B6F0CE82D2011F00077D37D /* SignInButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B6F0CE72D2011F00077D37D /* SignInButton.swift */; }; 9B8CA57D24B120CA009C86C2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9B8CA57C24B120CA009C86C2 /* LaunchScreen.storyboard */; }; A10D4E931B07948500A72D29 /* DatapointsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10D4E921B07948500A72D29 /* DatapointsTableView.swift */; }; A10DC2DF207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10DC2DE207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift */; }; A11A87C61FEBFF7200A43E47 /* ChooseGoalSortViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11A87C51FEBFF7200A43E47 /* ChooseGoalSortViewController.swift */; }; A11BC2D91FFAD5BC00E56064 /* TimerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11BC2D81FFAD5BC00E56064 /* TimerViewController.swift */; }; A12BA94E1AFF202200AFEF32 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A12BA94D1AFF202200AFEF32 /* SystemConfiguration.framework */; }; - A1453B3F1AEDFCC8006F48DA /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1453B3E1AEDFCC8006F48DA /* SignInViewController.swift */; }; A149147B1BE79FD50060600A /* EditNotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A149147A1BE79FD40060600A /* EditNotificationsViewController.swift */; }; A149147F1BE7A5670060600A /* SettingsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A149147E1BE7A5670060600A /* SettingsTableViewCell.swift */; }; A149B3701AEF528C00F19A09 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A149B36F1AEF528C00F19A09 /* SettingsViewController.swift */; }; @@ -218,7 +219,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 9B2488412D030E5100E610E0 /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = ""; }; 9B65F2312CFA6418009674A7 /* DeeplinkGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkGenerator.swift; sourceTree = ""; }; + 9B6F0CE72D2011F00077D37D /* SignInButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInButton.swift; sourceTree = ""; }; 9B8CA57C24B120CA009C86C2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; A10D4E921B07948500A72D29 /* DatapointsTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatapointsTableView.swift; sourceTree = ""; }; A10DC2DE207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveHKMetricViewController.swift; sourceTree = ""; }; @@ -233,7 +236,6 @@ A1453B341AED9184006F48DA /* UIColorExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColorExtension.swift; sourceTree = ""; }; A1453B381AEDA71D006F48DA /* BSLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BSLabel.swift; sourceTree = ""; }; A1453B3C1AEDFA52006F48DA /* CurrentUserManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrentUserManager.swift; sourceTree = ""; }; - A1453B3E1AEDFCC8006F48DA /* SignInViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignInViewController.swift; sourceTree = ""; }; A149147A1BE79FD40060600A /* EditNotificationsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditNotificationsViewController.swift; sourceTree = ""; }; A149147E1BE7A5670060600A /* SettingsTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewCell.swift; sourceTree = ""; }; A149B36F1AEF528C00F19A09 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; @@ -478,6 +480,7 @@ A196CB161AE4142E00B90A3E /* BeeSwift */ = { isa = PBXGroup; children = ( + 9B6F0CE72D2011F00077D37D /* SignInButton.swift */, 9B65F2312CFA6418009674A7 /* DeeplinkGenerator.swift */, A1E618E51E79E01900D8ED93 /* Cells */, E46071002B43DA7100305DB4 /* Gallery */, @@ -493,8 +496,8 @@ A196CB231AE4142F00B90A3E /* Images.xcassets */, 9B8CA57C24B120CA009C86C2 /* LaunchScreen.storyboard */, E43BEA832A036A9C00FC3A38 /* LogReader.swift */, - A1453B3E1AEDFCC8006F48DA /* SignInViewController.swift */, E4015D9E2D10DC4D00F58D94 /* SceneDelegate.swift */, + 9B2488412D030E5100E610E0 /* SignInView.swift */, ); path = BeeSwift; sourceTree = ""; @@ -1012,12 +1015,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - A1453B3F1AEDFCC8006F48DA /* SignInViewController.swift in Sources */, A1E618E41E7934C700D8ED93 /* HealthKitConfigTableViewCell.swift in Sources */, E4B083392932F90400A71564 /* ConfigureHKMetricViewController.swift in Sources */, E43BEA842A036A9C00FC3A38 /* LogReader.swift in Sources */, 9B65F2322CFA6427009674A7 /* DeeplinkGenerator.swift in Sources */, A196CB1F1AE4142F00B90A3E /* GalleryViewController.swift in Sources */, + 9B6F0CE82D2011F00077D37D /* SignInButton.swift in Sources */, A1BE73AA1E8B45BF00DEC4DB /* ChooseHKMetricViewController.swift in Sources */, A149147B1BE79FD50060600A /* EditNotificationsViewController.swift in Sources */, E4015D9F2D10DC4D00F58D94 /* SceneDelegate.swift in Sources */, @@ -1028,6 +1031,7 @@ A10DC2DF207BFCBA00FB7B3A /* RemoveHKMetricViewController.swift in Sources */, E412DAE12B86A8F70099E483 /* GoalImageView.swift in Sources */, A1619EA41BEECC1500E14B3A /* EditDefaultNotificationsViewController.swift in Sources */, + 9B2488422D030E5100E610E0 /* SignInView.swift in Sources */, A149B3701AEF528C00F19A09 /* SettingsViewController.swift in Sources */, E46DC80F2AA58DF20059FDFE /* PullToRefreshHint.swift in Sources */, A1E618E21E78158700D8ED93 /* HealthKitConfigViewController.swift in Sources */, diff --git a/BeeSwift/Gallery/GalleryViewController.swift b/BeeSwift/Gallery/GalleryViewController.swift index e8032803..65277955 100644 --- a/BeeSwift/Gallery/GalleryViewController.swift +++ b/BeeSwift/Gallery/GalleryViewController.swift @@ -13,7 +13,7 @@ import SwiftyJSON import HealthKit import SafariServices import OSLog - +import SwiftUI import BeeKit @@ -216,13 +216,17 @@ class GalleryViewController: UIViewController, UICollectionViewDelegateFlowLayou make.right.equalTo(self.view.safeAreaLayoutGuide.snp.rightMargin) make.bottom.equalTo(self.collectionView!.keyboardLayoutGuide.snp.top) } + + if !ServiceLocator.currentUserManager.signedIn(context: ServiceLocator.persistentContainer.viewContext) { + presentSignInScreen() + } } override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if !ServiceLocator.currentUserManager.signedIn(context: ServiceLocator.persistentContainer.viewContext) { - let signInVC = SignInViewController() - signInVC.modalPresentationStyle = .fullScreen - self.present(signInVC, animated: true, completion: nil) + presentSignInScreen() } else { self.updateGoals() self.fetchGoals() @@ -280,15 +284,34 @@ class GalleryViewController: UIViewController, UICollectionViewDelegateFlowLayou } @objc func handleSignOut() { + logger.debug("\(#function)") + self.goals = [] self.filteredGoals = [] self.collectionView?.reloadData() - if self.presentedViewController != nil { - if type(of: self.presentedViewController!) == SignInViewController.self { return } + + presentSignInScreen() + } + + + private var isSignInViewBeingPresented: Bool { + guard let presentedViewController else { return false } + + return type(of: presentedViewController) == UIHostingController.self + } + + + private func presentSignInScreen() { + let shallPresentSignIn = presentedViewController == nil || !isSignInViewBeingPresented + + guard shallPresentSignIn else { + logger.debug("\(#function) - already presenting sign in; not presenting it yet again") + return } - let signInVC = SignInViewController() - signInVC.modalPresentationStyle = .fullScreen - self.present(signInVC, animated: true, completion: nil) + + let hostingVC = UIHostingController(rootView: SignInView()) + hostingVC.modalPresentationStyle = .fullScreen + self.present(hostingVC, animated: true) } func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { @@ -511,4 +534,3 @@ class GalleryViewController: UIViewController, UICollectionViewDelegateFlowLayou self.fetchGoals() } } - diff --git a/BeeSwift/SignInButton.swift b/BeeSwift/SignInButton.swift new file mode 100644 index 00000000..f37165a5 --- /dev/null +++ b/BeeSwift/SignInButton.swift @@ -0,0 +1,33 @@ +import SwiftUI + +struct SignInButton: View { + @Environment(\.colorScheme) private var colorScheme + let action: () -> Void + let isDisabled: Bool + + init(action: @escaping () -> Void, isDisabled: Bool = false) { + self.action = action + self.isDisabled = isDisabled + } + + var body: some View { + Button(action: action) { + Label("Sign In", systemImage: "iphone.and.arrow.forward.inward") + .font(.system(size: 20)) + .frame(maxWidth: .infinity) + .frame(height: 44) + } + .buttonStyle(.bordered) + .foregroundStyle(colorScheme == .dark ? Color.yellow : Color.black) + .overlay { + if colorScheme == .dark { + RoundedRectangle(cornerRadius: 4) + .stroke(Color.yellow, lineWidth: 4) + } else { + RoundedRectangle(cornerRadius: 4) + .stroke(.clear) + } + } + .disabled(isDisabled) + } +} diff --git a/BeeSwift/SignInView.swift b/BeeSwift/SignInView.swift new file mode 100644 index 00000000..34a065c7 --- /dev/null +++ b/BeeSwift/SignInView.swift @@ -0,0 +1,122 @@ +// Part of BeeSwift. Copyright Beeminder + +import SwiftUI +import BeeKit + +struct SignInView: View { + @StateObject private var viewModel = SignInViewModel() + @State private var showingFailedSignInAlert = false + @State private var showingMissingDataAlert = false + @Environment(\.dismiss) private var dismiss + + var body: some View { + ZStack { + Color.yellow + .opacity(0.8) + .ignoresSafeArea() + + HStack(alignment: .center) { + + VStack(alignment: .center, spacing: 15) { + Spacer() + + Image("website_logo_mid") + .resizable() + .scaledToFit() + .frame(maxWidth: 400) + .padding(.bottom) + + VStack(spacing: 15) { + TextField("Email or username", text: $viewModel.email) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .keyboardType(.emailAddress) + .textContentType(.emailAddress) + .submitLabel(.next) + + SecureField("Password", text: $viewModel.password) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .textInputAutocapitalization(.never) + .submitLabel(.done) + + SignInButton(action: signIn, + isDisabled: viewModel.isLoading) + } + .frame(maxWidth: .infinity) + .padding(.horizontal, 40) + + Spacer() + Spacer() + Spacer() + } + .padding() + + } + .alert("Could not sign in", isPresented: $showingFailedSignInAlert) { + Button("OK", role: .cancel) { } + } message: { + Text("Invalid credentials") + } + .alert("Incomplete Account Details", isPresented: $showingMissingDataAlert) { + Button("OK", role: .cancel) { } + } message: { + Text("Username and Password are required") + } + .onReceive(NotificationCenter.default.publisher(for: Notification.Name(CurrentUserManager.failedSignInNotificationName))) { _ in + viewModel.isLoading = false + showingFailedSignInAlert = true + } + .onReceive(NotificationCenter.default.publisher(for: Notification.Name(CurrentUserManager.signedInNotificationName))) { _ in + viewModel.isLoading = false + dismiss() + } + } + .overlay { + if viewModel.isLoading { + ProgressView() + .scaleEffect(1.5) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.black.opacity(0.2)) + } + } + } + + private func signIn() { + guard !viewModel.email.trimmingCharacters(in: .whitespaces).isEmpty, + !viewModel.password.isEmpty else { + showingMissingDataAlert = true + return + } + + Task { + await viewModel.signIn() + } + } +} + +@MainActor +class SignInViewModel: ObservableObject { + @Published var email = "" + @Published var password = "" + @Published var isLoading = false + + func signIn() async { + isLoading = true + + do { + try await Task.sleep(nanoseconds: 3_000_000_000) + } catch { + print("canceled early") + } + + await ServiceLocator.currentUserManager.signInWithEmail( + email.trimmingCharacters(in: .whitespaces), + password: password + ) + } +} + +#Preview { + SignInView() +} diff --git a/BeeSwift/SignInViewController.swift b/BeeSwift/SignInViewController.swift deleted file mode 100644 index 8b53e894..00000000 --- a/BeeSwift/SignInViewController.swift +++ /dev/null @@ -1,154 +0,0 @@ -// -// SignInViewController.swift -// BeeSwift -// -// Created by Andy Brett on 4/26/15. -// Copyright (c) 2015 APB. All rights reserved. -// - -import Foundation -import MBProgressHUD -import SafariServices - -import BeeKit - -class SignInViewController : UIViewController, UITextFieldDelegate { - - var headerLabel = BSLabel() - var emailTextField = BSTextField() - var passwordTextField = BSTextField() - var beeImageView = UIImageView() - var signInButton = BSButton() - var divider = UIView() - - override func viewDidLoad() { - super.viewDidLoad() - - let scrollView = UIScrollView() - self.view.addSubview(scrollView) - scrollView.snp.makeConstraints { (make) -> Void in - make.edges.equalTo(self.view) - } - - NotificationCenter.default.addObserver(self, selector: #selector(self.handleFailedSignIn(_:)), name: NSNotification.Name(rawValue: CurrentUserManager.failedSignInNotificationName), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.handleSignedIn(_:)), name: NSNotification.Name(rawValue: CurrentUserManager.signedInNotificationName), object: nil) - self.view.backgroundColor = UIColor.systemBackground - - - self.beeImageView.image = UIImage(named: "website_logo_mid") - scrollView.addSubview(self.beeImageView) - self.beeImageView.snp.makeConstraints { (make) in - make.centerX.equalTo(scrollView) - make.centerY.equalToSuperview().multipliedBy(0.55) - } - - scrollView.addSubview(self.headerLabel) - self.headerLabel.textAlignment = NSTextAlignment.center - self.headerLabel.snp.makeConstraints { (make) -> Void in - make.top.equalTo(beeImageView.snp.bottom) - make.centerX.equalToSuperview() - } - - scrollView.addSubview(self.emailTextField) - self.emailTextField.isHidden = true - self.emailTextField.placeholder = "Email or username" - self.emailTextField.autocapitalizationType = .none - self.emailTextField.autocorrectionType = .no - self.emailTextField.keyboardType = UIKeyboardType.emailAddress - self.emailTextField.returnKeyType = .next - self.emailTextField.delegate = self - self.emailTextField.snp.makeConstraints { (make) -> Void in - make.top.equalTo(self.headerLabel.snp.bottom).offset(15) - make.centerX.equalTo(scrollView) - make.width.equalTo(scrollView).multipliedBy(0.75) - make.height.equalTo(Constants.defaultTextFieldHeight) - } - - scrollView.addSubview(self.passwordTextField) - self.passwordTextField.isHidden = true - self.passwordTextField.placeholder = "Password" - self.passwordTextField.isSecureTextEntry = true - self.passwordTextField.returnKeyType = .done - self.passwordTextField.autocapitalizationType = .none - self.passwordTextField.delegate = self - self.passwordTextField.snp.makeConstraints { (make) -> Void in - make.top.equalTo(self.emailTextField.snp.bottom).offset(15) - make.centerX.equalTo(self.emailTextField) - make.width.equalTo(self.emailTextField) - make.height.equalTo(Constants.defaultTextFieldHeight) - } - - scrollView.addSubview(self.signInButton) - self.signInButton.isHidden = true - self.signInButton.setTitle("Sign In", for: UIControl.State()) - self.signInButton.titleLabel?.font = UIFont.beeminder.defaultFontPlain.withSize(20) - self.signInButton.titleLabel?.textColor = UIColor.white - self.signInButton.addTarget(self, action: #selector(SignInViewController.signInButtonPressed), for: UIControl.Event.touchUpInside) - self.signInButton.snp.makeConstraints { (make) -> Void in - make.left.equalTo(self.passwordTextField) - make.right.equalTo(self.passwordTextField) - make.top.equalTo(self.passwordTextField.snp.bottom).offset(15) - make.height.equalTo(Constants.defaultTextFieldHeight) - } - - scrollView.addSubview(self.divider) - self.divider.isHidden = true - self.divider.backgroundColor = UIColor.Beeminder.gray - - self.chooseSignInButtonPressed() - } - - @objc func chooseSignInButtonPressed() { - self.emailTextField.isHidden = false - self.passwordTextField.isHidden = false - self.headerLabel.text = "Sign in to Beeminder" - self.headerLabel.isHidden = false - self.signInButton.isHidden = false - self.divider.snp.remakeConstraints { (make) -> Void in - make.left.equalTo(self.signInButton) - make.right.equalTo(self.signInButton) - make.height.equalTo(1) - make.top.equalTo(self.signInButton.snp.bottom).offset(15) - } - } - - var missingDataOnSignIn: UIAlertController { - let lackOfCredentials = UIAlertController(title: "Incomplete Account Details", message: "Username and Password are required", preferredStyle: .alert) - lackOfCredentials.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil)) - - return lackOfCredentials - } - - @objc func handleFailedSignIn(_ notification : Notification) { - let failureAC = UIAlertController(title: "Could not sign in", message: "Invalid credentials", preferredStyle: .alert) - failureAC.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil)) - self.present(failureAC, animated: true, completion: nil) - MBProgressHUD.hide(for: self.view, animated: true) - } - - @objc func handleSignedIn(_ notification : Notification) { - MBProgressHUD.hide(for: self.view, animated: true) - } - - @objc func signInButtonPressed() { - Task { @MainActor in - guard let email = self.emailTextField.text?.trimmingCharacters(in: .whitespaces), let password = self.passwordTextField.text, !email.isEmpty, !password.isEmpty else { - self.present(self.missingDataOnSignIn, animated: true, completion: nil) - return - } - - MBProgressHUD.showAdded(to: self.view, animated: true) - await ServiceLocator.currentUserManager.signInWithEmail(email, password: password) - } - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if textField.isEqual(self.emailTextField) { - self.passwordTextField.becomeFirstResponder() - } - else if textField.isEqual(self.passwordTextField) { - self.signInButtonPressed() - } - return true - } -}