From 7d0c3995b9b41ed47fb46c9d707aaf0d50e0506a Mon Sep 17 00:00:00 2001 From: Diego Chohfi Date: Mon, 4 Nov 2019 18:47:21 -0800 Subject: [PATCH 1/2] Add sharing state sample --- CombineFeedback.xcodeproj/project.pbxproj | 56 ++++++++ Example/Account/LoginView/LoginView.swift | 53 ++++++++ .../Account/LoginView/LoginViewModel.swift | 111 ++++++++++++++++ .../RegistrationView/RegistrationView.swift | 79 +++++++++++ .../RegistrationViewModel.swift | 125 ++++++++++++++++++ .../UserAccountView/UserAccountView.swift | 69 ++++++++++ .../UserAccountViewModel.swift | 68 ++++++++++ Example/SceneDelegate.swift | 15 ++- Example/SignIn/SignIn.swift | 4 +- Example/Views/Activity.swift | 10 +- 10 files changed, 583 insertions(+), 7 deletions(-) create mode 100644 Example/Account/LoginView/LoginView.swift create mode 100644 Example/Account/LoginView/LoginViewModel.swift create mode 100644 Example/Account/RegistrationView/RegistrationView.swift create mode 100644 Example/Account/RegistrationView/RegistrationViewModel.swift create mode 100644 Example/Account/UserAccountView/UserAccountView.swift create mode 100644 Example/Account/UserAccountView/UserAccountViewModel.swift diff --git a/CombineFeedback.xcodeproj/project.pbxproj b/CombineFeedback.xcodeproj/project.pbxproj index 4c7acb0..934f4ba 100644 --- a/CombineFeedback.xcodeproj/project.pbxproj +++ b/CombineFeedback.xcodeproj/project.pbxproj @@ -39,6 +39,12 @@ 58C6E5E022A9F027005A9685 /* Movies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6E5DF22A9F027005A9685 /* Movies.swift */; }; 58C6E5E722AB14DB005A9685 /* AsyncImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6E5E622AB14DB005A9685 /* AsyncImage.swift */; }; 7415E15122E1B4F000117DA3 /* TestScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74A3948222E0B7E20061CE51 /* TestScheduler.swift */; }; + C3B78C7D2371170D00D91F95 /* RegistrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B78C752371170D00D91F95 /* RegistrationView.swift */; }; + C3B78C7E2371170D00D91F95 /* RegistrationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B78C762371170D00D91F95 /* RegistrationViewModel.swift */; }; + C3B78C7F2371170D00D91F95 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B78C782371170D00D91F95 /* LoginView.swift */; }; + C3B78C802371170D00D91F95 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B78C792371170D00D91F95 /* LoginViewModel.swift */; }; + C3B78C812371170D00D91F95 /* UserAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B78C7B2371170D00D91F95 /* UserAccountViewModel.swift */; }; + C3B78C822371170D00D91F95 /* UserAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B78C7C2371170D00D91F95 /* UserAccountView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -159,6 +165,12 @@ 58C6E5E622AB14DB005A9685 /* AsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncImage.swift; sourceTree = ""; }; 742D7D6522B02A7100A97AF3 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 74A3948222E0B7E20061CE51 /* TestScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestScheduler.swift; sourceTree = ""; }; + C3B78C752371170D00D91F95 /* RegistrationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegistrationView.swift; sourceTree = ""; }; + C3B78C762371170D00D91F95 /* RegistrationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegistrationViewModel.swift; sourceTree = ""; }; + C3B78C782371170D00D91F95 /* LoginView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; + C3B78C792371170D00D91F95 /* LoginViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; + C3B78C7B2371170D00D91F95 /* UserAccountViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserAccountViewModel.swift; sourceTree = ""; }; + C3B78C7C2371170D00D91F95 /* UserAccountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserAccountView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -281,6 +293,7 @@ 5800FFB722A8CDA5005A860B /* Example */ = { isa = PBXGroup; children = ( + C3B78C732371170D00D91F95 /* Account */, 583971BD22ADB9DF00139CC0 /* Helpers */, 58C6E5E522AB149D005A9685 /* Views */, 58C6E5DE22A9EFDF005A9685 /* CounterExample */, @@ -393,6 +406,43 @@ path = Tests; sourceTree = ""; }; + C3B78C732371170D00D91F95 /* Account */ = { + isa = PBXGroup; + children = ( + C3B78C742371170D00D91F95 /* RegistrationView */, + C3B78C772371170D00D91F95 /* LoginView */, + C3B78C7A2371170D00D91F95 /* UserAccountView */, + ); + path = Account; + sourceTree = ""; + }; + C3B78C742371170D00D91F95 /* RegistrationView */ = { + isa = PBXGroup; + children = ( + C3B78C752371170D00D91F95 /* RegistrationView.swift */, + C3B78C762371170D00D91F95 /* RegistrationViewModel.swift */, + ); + path = RegistrationView; + sourceTree = ""; + }; + C3B78C772371170D00D91F95 /* LoginView */ = { + isa = PBXGroup; + children = ( + C3B78C782371170D00D91F95 /* LoginView.swift */, + C3B78C792371170D00D91F95 /* LoginViewModel.swift */, + ); + path = LoginView; + sourceTree = ""; + }; + C3B78C7A2371170D00D91F95 /* UserAccountView */ = { + isa = PBXGroup; + children = ( + C3B78C7B2371170D00D91F95 /* UserAccountViewModel.swift */, + C3B78C7C2371170D00D91F95 /* UserAccountView.swift */, + ); + path = UserAccountView; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -658,14 +708,20 @@ buildActionMask = 2147483647; files = ( 25C57B2C22BC2C33007CB4D6 /* Activity.swift in Sources */, + C3B78C7E2371170D00D91F95 /* RegistrationViewModel.swift in Sources */, 58C6E5E022A9F027005A9685 /* Movies.swift in Sources */, + C3B78C822371170D00D91F95 /* UserAccountView.swift in Sources */, 252BF08422BAE05700BC4265 /* SignIn.swift in Sources */, + C3B78C802371170D00D91F95 /* LoginViewModel.swift in Sources */, 5800FFB922A8CDA5005A860B /* AppDelegate.swift in Sources */, 5800FFBB22A8CDA5005A860B /* SceneDelegate.swift in Sources */, + C3B78C812371170D00D91F95 /* UserAccountViewModel.swift in Sources */, + C3B78C7F2371170D00D91F95 /* LoginView.swift in Sources */, 583971C322ADBA9900139CC0 /* PublisherExtensions.swift in Sources */, 58C6E5E722AB14DB005A9685 /* AsyncImage.swift in Sources */, 5800FFBD22A8CDA5005A860B /* Counter.swift in Sources */, 25F23C2922CA984E00894863 /* TrafficLight.swift in Sources */, + C3B78C7D2371170D00D91F95 /* RegistrationView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/Account/LoginView/LoginView.swift b/Example/Account/LoginView/LoginView.swift new file mode 100644 index 0000000..045cea6 --- /dev/null +++ b/Example/Account/LoginView/LoginView.swift @@ -0,0 +1,53 @@ +import Foundation +import SwiftUI +import Combine +import CombineFeedback +import CombineFeedbackUI + +public struct LoginView: View { + public typealias State = LoginViewModel.State + public typealias Event = LoginViewModel.Event + + private let context: Context + + public init(context: Context) { + self.context = context + } + + private var submitButton: some View { + Button("Login") { self.context.send(event: .login) } + } + + private var emailSection: some View { + Section(header: Text("Email"), footer: Text(self.context.invalidEmailMessage)) { + TextField("Email", text: self.context.binding(for: \.email)) + .keyboardType(.emailAddress) + .textContentType(.emailAddress) + .autocapitalization(.none) + } + } + + private var passwordSection: some View { + Section(header: Text("Password"), footer: Text(self.context.invalidPasswordMessage)) { + SecureField("Password", text: self.context.binding(for: \.password)) + .textContentType(.newPassword) + } + } + + private func welcomeAlert(alert: Alert) -> SwiftUI.Alert { + SwiftUI.Alert(title: Text(alert.title), message: Text(alert.message), dismissButton: .default(Text("OK")) { + self.context.send(event: .dismiss) + }) + } + + public var body: some View { + Form { + self.emailSection + self.passwordSection + self.submitButton + } + .alert(item: .constant(self.context.alertMessage), content: self.welcomeAlert(alert:)) + .navigationBarItems(trailing: Activity(isAnimating: .constant(self.context.status.isLoading), style: .medium)) + .navigationBarTitle(Text("Account")) + } +} diff --git a/Example/Account/LoginView/LoginViewModel.swift b/Example/Account/LoginView/LoginViewModel.swift new file mode 100644 index 0000000..8480532 --- /dev/null +++ b/Example/Account/LoginView/LoginViewModel.swift @@ -0,0 +1,111 @@ +import Foundation +import Combine +import CombineFeedback +import CombineFeedbackUI + +extension LoginViewModel { + public struct State { + var email = "" + var password = "" + var triedToSubmitOnce = false + var status = Status.idle + var loggedAccount: LoggedAccount? + var dismissed: Bool = false + + public init() { + } + + var alertMessage: Alert? { + guard let loggedAccount = loggedAccount else { + return nil + } + return Alert(title: "Welcome back", message: "\(loggedAccount.firstName)") + } + + var invalidEmailMessage: String { + invalidEmail ? "Invalid E-mail" : "" + } + + var invalidPasswordMessage: String { + invalidPassword ? "Invalid Password" : "" + } + + var valid: Bool { + invalidEmail == false && invalidPassword == false + } + + private var invalidEmail: Bool { + triedToSubmitOnce && !email.validEmail + } + + private var invalidPassword: Bool { + triedToSubmitOnce && password.count <= 5 + } + } + + public enum Status { + case idle + case loading + + var isLoading: Bool { + if case .loading = self { + return true + } + return false + } + } + + public enum Event { + case login + case didSignin(LoggedAccount) + case dismiss + } +} + +public final class LoginViewModel: ViewModel { + + init(state: State = State()) { + super.init( + initial: state, + feedbacks: [LoginViewModel.whenSigning()], + scheduler: RunLoop.main, + reducer: LoginViewModel.reducer(state:event:) + ) + } + + private static func whenSigning() -> Feedback { + Feedback(predicate: { $0.status.isLoading }, effects: { (state: State) in + Just(()) + .delay(for: 2, scheduler: RunLoop.main) + .map { _ in + LoggedAccount(email: state.email, + password: state.password, + firstName: "Diego", + lastName: "Chohfi") + } + .map(Event.didSignin) + .eraseToAnyPublisher() + }) + } + + private static func reducer(state: State, event: Event) -> State { + switch event { + case .login: + var state = state + state.triedToSubmitOnce = true + if state.valid { + state.status = .loading + } + return state + case let .didSignin(account): + var state = state + state.status = .idle + state.loggedAccount = account + return state + case .dismiss: + var state = state + state.dismissed = true + return state + } + } +} diff --git a/Example/Account/RegistrationView/RegistrationView.swift b/Example/Account/RegistrationView/RegistrationView.swift new file mode 100644 index 0000000..8d36757 --- /dev/null +++ b/Example/Account/RegistrationView/RegistrationView.swift @@ -0,0 +1,79 @@ +import Foundation +import SwiftUI +import Combine +import CombineFeedback +import CombineFeedbackUI + +public struct RegistrationView: View { + public typealias State = RegistrationViewModel.State + public typealias Event = RegistrationViewModel.Event + + private let context: Context + private let loginViewModel: LoginViewModel + + public init(context: Context, loginViewModel: LoginViewModel) { + self.context = context + self.loginViewModel = loginViewModel + } + + private var submitButton: some View { + Button("Create Account", action: { self.context.send(event: .register) }) + } + + private var nameSection: some View { + Section(header: Text("You")) { + HStack { + TextField("First name", text: self.context.binding(for: \.firstName)) + TextField("Last name", text: self.context.binding(for: \.lastName)) + } + } + } + + private var emailSection: some View { + Section(header: Text("Email"), footer: Text(self.context.invalidEmailMessage)) { + TextField("Email", text: self.context.binding(for: \.email)) + .keyboardType(.emailAddress) + .textContentType(.emailAddress) + .autocapitalization(.none) + } + } + + private var passwordSection: some View { + Section(header: Text("Password"), footer: Text(self.context.invalidPasswordMessage)) { + SecureField("Password", text: self.context.binding(for: \.password)) + .textContentType(.newPassword) + } + } + + private var navigateToSignin: some View { + HStack { + Activity(isAnimating: .constant(self.context.status.isLoading), style: .medium) + NavigationLink("Sign In", destination: Widget(viewModel: self.loginViewModel, render: LoginView.init)) + } + } + + private var dismissButton: some View { + Button("Dismiss") { self.context.send(event: .dismiss) } + } + + private func welcomeAlert(alert: Alert) -> SwiftUI.Alert { + SwiftUI.Alert(title: Text(alert.title), message: Text(alert.message), dismissButton: .default(Text("OK")) { + self.context.send(event: .dismiss) + }) + } + + public var body: some View { + NavigationView { + Form { + self.nameSection + self.emailSection + self.passwordSection + self.submitButton + } + .disabled(self.context.status.isLoading) + .alert(item: .constant(self.context.alertMessage), content: self.welcomeAlert(alert:)) + .navigationBarTitle(Text("Account")) + .navigationBarItems(leading: self.dismissButton, trailing: self.navigateToSignin) + } + } +} diff --git a/Example/Account/RegistrationView/RegistrationViewModel.swift b/Example/Account/RegistrationView/RegistrationViewModel.swift new file mode 100644 index 0000000..c7fe3d9 --- /dev/null +++ b/Example/Account/RegistrationView/RegistrationViewModel.swift @@ -0,0 +1,125 @@ +import Foundation +import Combine +import CombineFeedback +import CombineFeedbackUI + +extension String { + var validEmail: Bool { + let regexp = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let predicate = NSPredicate(format: "SELF MATCHES %@", regexp) + return predicate.evaluate(with: self) + } + + var validPassword: Bool { + count > 5 + } +} + +public struct Alert: Identifiable { + var title: String + var message: String + public var id: String { message } +} + + +public final class RegistrationViewModel: ViewModel { + + public struct State { + var email: String = "" + var password: String = "" + var firstName: String = "" + var lastName: String = "" + var triedToSubmitOnce = false + var status = Status.idle + var loggedAccount: LoggedAccount? + var dismissed: Bool = false + + public init() { + } + + var alertMessage: Alert? { + guard let loggedAccount = loggedAccount else { + return nil + } + return Alert(title: "Account created", message: "Welcome \(loggedAccount.email)") + } + + var invalidEmailMessage: String { + invalidEmail ? "Invalid E-mail" : "" + } + + var invalidPasswordMessage: String { + invalidPassword ? "Invalid Password" : "" + } + + var valid: Bool { + invalidEmail == false && invalidPassword == false + } + + private var invalidEmail: Bool { + triedToSubmitOnce && !email.validEmail + } + + private var invalidPassword: Bool { + triedToSubmitOnce && password.count <= 5 + } + } + + public enum Status { + case idle + case loading + + var isLoading: Bool { + return self == .loading + } + } + + public enum Event { + case register + case didRegister(LoggedAccount) + case dismiss + } + + public init(state: State = State()) { + super.init( + initial: state, + feedbacks: [RegistrationViewModel.whenSubmiting()], + scheduler: RunLoop.main, + reducer: RegistrationViewModel.reducer(state:event:) + ) + } + + private static func whenSubmiting() -> Feedback { + return Feedback(predicate: { $0.status.isLoading }, effects: { state in + Just("Account Created") + .delay(for: 2, scheduler: RunLoop.main) + .map { _ in + LoggedAccount(email: state.email, + password: state.password, + firstName: state.firstName, + lastName: state.lastName) + } + .map(Event.didRegister) + .eraseToAnyPublisher() + }) + } + + private static func reducer(state: State, event: Event) -> State { + switch event { + case .register: + var state = state + state.triedToSubmitOnce = true + state.status = state.valid ? .loading : .idle + return state + case let .didRegister(loggedAccount): + var state = state + state.status = .idle + state.loggedAccount = loggedAccount + return state + case .dismiss: + var state = state + state.dismissed = true + return state + } + } +} diff --git a/Example/Account/UserAccountView/UserAccountView.swift b/Example/Account/UserAccountView/UserAccountView.swift new file mode 100644 index 0000000..6807021 --- /dev/null +++ b/Example/Account/UserAccountView/UserAccountView.swift @@ -0,0 +1,69 @@ +import Foundation +import SwiftUI +import Combine +import CombineFeedback +import CombineFeedbackUI + +public struct UserAccountView: View { + + public typealias State = UserAccountViewModel.State + public typealias Event = UserAccountViewModel.Event + + private let context: Context + + private let loginViewModel: LoginViewModel + private let registrationViewModel: RegistrationViewModel + + private let authenticationSubject: PassthroughSubject = PassthroughSubject() + + private let cancellable: AnyCancellable + + public init(context: Context) { + self.context = context + self.loginViewModel = LoginViewModel() + self.registrationViewModel = RegistrationViewModel() + + self.cancellable = Publishers.Merge(self.loginViewModel.state.filter { $0.dismissed }.map(\.loggedAccount), + self.registrationViewModel.state.filter { $0.dismissed }.map(\.loggedAccount)) + .receive(on: RunLoop.main) + .sink { loggedAccount in + if let loggedAccount = loggedAccount { + context.send(event: .didAuthenticate(loggedAccount)) + } + context.send(event: .dismiss) + } + } + + private var registerButton: some View { + Button("Register") { self.context.send(event: .register) } + } + + private func accountButton(account: LoggedAccount) -> some View { + Button("Logout from \(account.email)") { self.context.send(event: .signout) } + } + + public var body: some View { + Form { + Section(header: Text("Account")) { + if self.context.loggedAccount == nil { + self.registerButton + } else { + self.accountButton(account: self.context.loggedAccount!) + } + } + } + .sheet( + isPresented: .constant(self.context.screen.isSigningUp), + onDismiss: { self.context.send(event: .dismiss) }, + content: { + Widget( + viewModel: self.registrationViewModel, + render: { + RegistrationView(context: $0, loginViewModel: self.loginViewModel) + } + ) + } + ) + .navigationBarTitle(Text("Account"), displayMode: .inline) + } +} diff --git a/Example/Account/UserAccountView/UserAccountViewModel.swift b/Example/Account/UserAccountView/UserAccountViewModel.swift new file mode 100644 index 0000000..df78ea5 --- /dev/null +++ b/Example/Account/UserAccountView/UserAccountViewModel.swift @@ -0,0 +1,68 @@ +import Foundation +import Combine +import CombineFeedback +import CombineFeedbackUI + +public struct LoggedAccount: Equatable { + let email: String + let password: String + let firstName: String + let lastName: String +} + +extension UserAccountViewModel { + public struct State { + var screen: Screen = .home + var loggedAccount: LoggedAccount? + } + + public enum Event { + case dismiss + case register + case didAuthenticate(LoggedAccount) + case signout + } + + public enum Screen { + case home + case signingUp + + var isSigningUp: Bool { + if case .signingUp = self { + return true + } + return false + } + } +} + +public final class UserAccountViewModel: ViewModel { + public init() { + let initial = State() + super.init(initial: initial, + feedbacks: [], + scheduler: RunLoop.main, + reducer: UserAccountViewModel.reducer(state:event:)) + } + + private static func reducer(state: State, event: Event) -> State { + switch event { + case .dismiss: + var state = state + state.screen = .home + return state + case .register: + var state = state + state.screen = .signingUp + return state + case let .didAuthenticate(account): + var state = state + state.loggedAccount = account + return state + case .signout: + var state = state + state.loggedAccount = nil + return state + } + } +} diff --git a/Example/SceneDelegate.swift b/Example/SceneDelegate.swift index e09735f..d1a9870 100644 --- a/Example/SceneDelegate.swift +++ b/Example/SceneDelegate.swift @@ -63,7 +63,20 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { selectedImage: UIImage(systemName: "tortoise.fill") ) - tabbarController.viewControllers = [counter, movies, signIn, trafficLight] + let account = UIHostingController( + rootView: NavigationView { + return Widget(viewModel: UserAccountViewModel(), + render: UserAccountView.init) + } + ) + + account.tabBarItem = UITabBarItem( + title: nil, + image: UIImage(systemName: "at"), + selectedImage: UIImage(systemName: "at.fill") + ) + + tabbarController.viewControllers = [counter, movies, signIn, trafficLight, account] window.rootViewController = tabbarController self.window = window window.makeKeyAndVisible() diff --git a/Example/SignIn/SignIn.swift b/Example/SignIn/SignIn.swift index 25f2df6..e530168 100644 --- a/Example/SignIn/SignIn.swift +++ b/Example/SignIn/SignIn.swift @@ -155,7 +155,7 @@ struct SignInView: View { .textContentType(.username) Group { if context.status.isCheckingUserName { - Activity(style: .medium) + Activity(isAnimating: .constant(true), style: .medium) } else { Image(systemName: context.isAvailable ? "hand.thumbsup.fill" : "xmark.seal.fill") } @@ -210,7 +210,7 @@ struct SignInView: View { } Group { if context.status.isSubmitting { - Activity(style: .medium) + Activity(isAnimating: .constant(true), style: .medium) } else { EmptyView() } diff --git a/Example/Views/Activity.swift b/Example/Views/Activity.swift index 5e26229..b4c7123 100644 --- a/Example/Views/Activity.swift +++ b/Example/Views/Activity.swift @@ -2,8 +2,8 @@ import SwiftUI struct Activity: UIViewRepresentable { typealias UIViewType = UIActivityIndicatorView - - var style: UIActivityIndicatorView.Style + var isAnimating: Binding + let style: UIActivityIndicatorView.Style func makeUIView(context: UIViewRepresentableContext) -> UIActivityIndicatorView { let view = UIActivityIndicatorView(style: style) @@ -11,13 +11,15 @@ struct Activity: UIViewRepresentable { return view } - func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) {} + public func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext) { + isAnimating.wrappedValue ? uiView.startAnimating() : uiView.stopAnimating() + } } #if DEBUG struct Activity_Previews : PreviewProvider { static var previews: some View { - Activity(style: .medium) + Activity(isAnimating: .constant(true), style: .medium) } } #endif From 29aaedc5e110e069ac01f104b70163a613497387 Mon Sep 17 00:00:00 2001 From: Diego Chohfi Date: Mon, 4 Nov 2019 18:54:46 -0800 Subject: [PATCH 2/2] Removing unecessary passrhough object --- Example/Account/UserAccountView/UserAccountView.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Example/Account/UserAccountView/UserAccountView.swift b/Example/Account/UserAccountView/UserAccountView.swift index 6807021..e9e955d 100644 --- a/Example/Account/UserAccountView/UserAccountView.swift +++ b/Example/Account/UserAccountView/UserAccountView.swift @@ -14,8 +14,6 @@ public struct UserAccountView: View { private let loginViewModel: LoginViewModel private let registrationViewModel: RegistrationViewModel - private let authenticationSubject: PassthroughSubject = PassthroughSubject() - private let cancellable: AnyCancellable public init(context: Context) {