I\'m developing a simple iOS app with SwiftUI with two views: a LogInView()
and a HomeView()
.
What I want is really simple: when the user c
Update: I got some time to update the answer, and add a transition. Note that I changed Group by VStack, otherwise the transition does not work.
You can alter the duration in the withAnimation
call (button closure).
I also moved some modifiers in your button, so the whole thing is "tappable". Otherwise, only tapping on the text of the button would trigger the action.
You can use an ObservedObject
, an EnvironmentObject
or a Binding
. Here's an example with ObservedObject
:
import SwiftUI
class Model: ObservableObject {
@Published var loggedIn = false
}
struct ContentView: View {
@ObservedObject var model = Model()
var body: some View {
VStack {
if model.loggedIn {
HomeView().transition(.opacity)
} else {
LogInView(model: model).transition(.opacity)
}
}
}
}
struct HomeView: View {
var body: some View {
Text("Home Page")
}
}
struct LogInView: View {
@ObservedObject var model: Model
var body: some View {
VStack {
Text("Welcome to Mamoot!")
.font(.largeTitle)
.fontWeight(.heavy)
Text("We are glad to have you here.")
Text("Please log in with your Mastodon or Twitter account to continue.")
.multilineTextAlignment(.center)
.lineLimit(4)
.padding()
Spacer()
// FloatingTextField(title: "Username", placeholder: "Username", width: 300, type: "Username")
// FloatingTextField(title: "Password", placeholder: "Password", width: 300, type: "password")
// .padding(.top, -50)
Spacer()
ZStack {
Button(action: {
withAnimation(.easeInOut(duration: 1.0)) {
self.model.loggedIn = true
}
}) {
Text("Log in")
.foregroundColor(Color.white)
.bold()
.shadow(color: .red, radius: 10)
// moved modifiers here, so the whole button is tappable
.padding(.leading, 140)
.padding(.trailing, 140)
.padding(.top, 15)
.padding(.bottom, 15)
.background(Color.red)
.cornerRadius(10)
}
}
.padding(.bottom)
}
}
}
The answer by @kontiki is probably the most SwiftUI-y, but I will present a different solution, probably not as good! But maybe more flexible/scalable.
You can swap rootView of UIHostingController:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
fileprivate lazy var appCoordinator: AppCoordinator = {
let rootViewController: UIHostingController<AnyView> = .init(rootView: EmptyView().eraseToAny())
window?.rootViewController = rootViewController
let navigationHandler: (AnyScreen, TransitionAnimation) -> Void = { [unowned rootViewController, window] (newRootScreen: AnyScreen, transitionAnimation: TransitionAnimation) in
UIView.transition(
with: window!,
duration: 0.5,
options: transitionAnimation.asUIKitTransitionAnimation,
animations: { rootViewController.rootView = newRootScreen },
completion: nil
)
}
return AppCoordinator(
dependencies: (
securePersistence: KeyValueStore(KeychainSwift()),
preferences: .default
),
navigator: navigationHandler
)
}()
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
self.window = .fromScene(scene)
appCoordinator.start()
}
}
enum TransitionAnimation {
case flipFromLeft
case flipFromRight
}
private extension TransitionAnimation {
var asUIKitTransitionAnimation: UIView.AnimationOptions {
switch self {
case .flipFromLeft: return UIView.AnimationOptions.transitionFlipFromLeft
case .flipFromRight: return UIView.AnimationOptions.transitionFlipFromRight
}
}
}
And here is the AppCoordinator:
final class AppCoordinator {
private let preferences: Preferences
private let securePersistence: SecurePersistence
private let navigationHandler: (AnyScreen, TransitionAnimation) -> Void
init(
dependencies: (securePersistence: SecurePersistence, preferences: Preferences),
navigator navigationHandler: @escaping (AnyScreen, TransitionAnimation) -> Void
) {
self.preferences = dependencies.preferences
self.securePersistence = dependencies.securePersistence
self.navigationHandler = navigationHandler
}
}
// MARK: Internal
internal extension AppCoordinator {
func start() {
navigate(to: initialDestination)
}
}
// MARK: Destination
private extension AppCoordinator {
enum Destination {
case welcome, getStarted, main
}
func navigate(to destination: Destination, transitionAnimation: TransitionAnimation = .flipFromLeft) {
let screen = screenForDestination(destination)
navigationHandler(screen, transitionAnimation)
}
func screenForDestination(_ destination: Destination) -> AnyScreen {
switch destination {
case .welcome: return AnyScreen(welcome)
case .getStarted: return AnyScreen(getStarted)
case .main: return AnyScreen(main)
}
}
var initialDestination: Destination {
guard preferences.hasAgreedToTermsAndPolicy else {
return .welcome
}
guard securePersistence.isAccountSetup else {
return .getStarted
}
return .main
}
}
// MARK: - Screens
private extension AppCoordinator {
var welcome: some Screen {
WelcomeScreen()
.environmentObject(
WelcomeViewModel(
preferences: preferences,
termsHaveBeenAccepted: { [unowned self] in self.start() }
)
)
}
var getStarted: some Screen {
GetStartedScreen()
.environmentObject(
GetStartedViewModel(
preferences: preferences,
securePersistence: securePersistence,
walletCreated: { [unowned self] in self.navigate(to: .main) }
)
)
}
var main: some Screen {
return MainScreen().environmentObject(
MainViewModel(
preferences: preferences,
securePersistence: securePersistence,
walletDeleted: { [unowned self] in
self.navigate(to: .getStarted, transitionAnimation: .flipFromRight)
}
)
)
}
}