Assuming you've got two basic views (e.g., a LoginView
and a MainView
), you can transition between them in a couple ways. What you'll need is:
- Some sort of state that determines which is being shown
- Some wrapping view that will transition between two layouts when #1 changes
- Some way of communicating data between the views
In this answer, I'll combine #1 & #3 in a model object, and show two examples for #2. There are lots of ways you could make this happen, so play around and see what works best for you.
Note that there is a lot of code just to style the views, so you can see what's going on. I've commented the critical bits.
Pictures (opacity method on left, offset method on right)
![Offset Transition](https://i.stack.imgur.com/JsBA3.gif)
The model (this satisfies #1 & #3)
class LoginStateModel: ObservableObject {
// changing this will change the main view
@Published var loggedIn = false
// will store the username typed on the LoginView
@Published var username = ""
func login() {
// simulating successful API call
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// when we log in, animate the result of setting loggedIn to true
// (i.e., animate showing MainView)
withAnimation(.default) {
self.loggedIn = true
}
}
}
}
The top-level view (this satisfies #2)
struct ContentView: View {
@ObservedObject var model = LoginStateModel()
var body: some View {
ZStack {
// just here for background
Color(UIColor.cyan).opacity(0.3)
.edgesIgnoringSafeArea(.all)
// we show either LoginView or MainView depending on our model
if model.loggedIn {
MainView()
} else {
LoginView()
}
}
// this passes the model down to descendant views
.environmentObject(model)
}
}
The default transition for adding and removing views from the view hierarchy is to change their opacity. Since we wrapped our changes to model.loggedIn
in withAnimation(.default)
, this opacity change will happen slowly (its better on a real device than the compressed GIFs below).
Alternatively, instead of having the views fade in/out, we could have them move on/off screen using an offset. For the second example, replace the if
/else
block above (including the if
itself) with
MainView()
.offset(x: model.loggedIn ? 0 : UIScreen.main.bounds.width, y: 0)
LoginView()
.offset(x: model.loggedIn ? -UIScreen.main.bounds.width : 0, y: 0)
The login view
struct LoginView: View {
@EnvironmentObject var model: LoginStateModel
@State private var usernameString = ""
@State private var passwordString = ""
var body: some View {
VStack(spacing: 15) {
HStack {
Text("Username")
Spacer()
TextField("Username", text: $usernameString)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
HStack {
Text("Password")
Spacer()
SecureField("Password", text: $passwordString)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
Button(action: {
// save the entered username, and try to log in
self.model.username = self.usernameString
self.model.login()
}, label: {
Text("Login")
.font(.title)
.inExpandingRectangle(Color.blue.opacity(0.6))
})
.buttonStyle(PlainButtonStyle())
}
.padding()
.inExpandingRectangle(Color.gray)
.frame(width: 300, height: 200)
}
}
Note that in a real functional login form, you'd want to do some basic input sanitation and disable/rate limit the login button so you don't get a billion server requests if someone spams the button.
For inspiration, see:
Introducing Combine (WWDC Session)
Combine in Practice (WWDC Session)
Using Combine (UIKit example, but shows how to throttle network requests)
The main view
struct MainView: View {
@EnvironmentObject var model: LoginStateModel
var body: some View {
VStack(spacing: 15) {
ZStack {
Text("Hello (model.username)!")
.font(.title)
.inExpandingRectangle(Color.blue.opacity(0.6))
.frame(height: 60)
HStack {
Spacer()
Button(action: {
// when we log out, animate the result of setting loggedIn to false
// (i.e., animate showing LoginView)
withAnimation(.default) {
self.model.loggedIn = false
}
}, label: {
Text("Logout")
.inFittedRectangle(Color.green.opacity(0.6))
})
.buttonStyle(PlainButtonStyle())
.padding()
}
}
Text("Content")
.inExpandingRectangle(.gray)
}
.padding()
}
}
Some convenience extensions
extension View {
func inExpandingRectangle(_ color: Color) -> some View {
ZStack {
RoundedRectangle(cornerRadius: 15)
.fill(color)
self
}
}
func inFittedRectangle(_ color: Color) -> some View {
self
.padding(5)
.background(RoundedRectangle(cornerRadius: 15)
.fill(color))
}
}