I am a beginner to swift, and i want to swap between 2 views.
Here is my current code.
I have tried to look at other stackoverflow questions but I dont understand what a viewcontroller or anything is, so if you do answer please explain what it does.
All i want to do is swap from ContentView to MessageView when the sign up button is pressed, but I don't know how to approach this at all.
import SwiftUI
struct ContentView: View {
#State private var username: String = ""
#State private var password: String = ""
var body: some View {
VStack {
Text("Create an account.")
TextField("Username", text: $username)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 250, height: /*#START_MENU_TOKEN#*/100/*#END_MENU_TOKEN#*/)
SecureField("Password", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 250, height: /*#START_MENU_TOKEN#*/100/*#END_MENU_TOKEN#*/)
let button = Button("Sign Up!") {
if(username == "" || password == "") {
print("no suername or password")
} else {
print("user", username)
print("password", password)
}
}
button.cornerRadius(5.0)
}
}
}
struct MessageView: View {
var body: some View {
NavigationView {
List {
Text("FortniteGamer123")
Text("FortniteEpicKid")
Text("KidIsGoodAtFortnite")
}
}.navigationTitle("Fortnite Gamers")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If anyone could at least give me some help on view controllers and how to use them that would be great.
You're using SwiftUI, which doesn't really have a concept of view controllers (or at least UIViewController), which are more part of UIKit.
SwiftUI, rather, is made up of sets of composable views. You already have a couple that you've defined (like ContentView and MessageView) and are using a bunch of built-in SwiftUI views (like TextField, Text, Button, etc).
To swap between views, you can use an if statement that is dependent on a #State variable.
I refactored things a bit so that LoginView and MessageView are shown by a parent view (ContentView) depending on the state of loggedIn. This is passed via a Binding to LoginView so that when the button is pressed, the value can get passed back up to the parent.
struct ContentView: View { //parent view
#State var loggedIn = false //state variable
var body: some View {
if loggedIn { //'if' condition
MessageView()
} else {
LoginView(loggedIn: $loggedIn) //passing loggedIn with the $ sign makes it a two-way binding where the child view can now have access to and change the variable
}
}
}
struct LoginView : View {
#Binding var loggedIn : Bool //this is how loggedIn gets passed in
#State private var username: String = ""
#State private var password: String = ""
var body: some View {
VStack {
Text("Create an account.")
TextField("Username", text: $username)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 250, height: 100)
SecureField("Password", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 250, height: 100)
Button(action: {
if(username == "" || password == "") {
print("no suername or password")
} else {
print("user", username)
print("password", password)
loggedIn = true //set the binding to true, which will effect the parent view
}
}) {
Text("Sign Up!")
}
.padding()
.overlay(RoundedRectangle(cornerRadius: 5.0).stroke(Color.accentColor)) //using this to round the corners
}
}
}
Technically, you could've also approached this by using NavigationView and NavigationLink, but that would've resulted in a situation where the user could've tapped the back button to go back to the login screen, which isn't really something you see in iOS, so I took your title literally about swapping between the two views.
I made a couple very minor edits to your other code. For example, had a let Button = in the middle of your view hierarchy that I've adjusted -- normally in SwiftUI you won't be defining variables that capture the views -- instead, you'll just be defining them in the hierarchy.
In Swift we have two main framework to build iOS applications: UIKit and SwiftUI. The first one is imperative and the second one - declarative. In UIKit we are using controllers and views classes to create UI and in SwiftUI we are using View structures to declare how we want user interface to show up. Basically SwiftUI in this case is similar to React / React Native and Flutter. In you example you are using SwiftUI, so you need to declare state variable and show the view based on this variable value, for example using .sheet(isPresented: <your state var>) or if statement. Also you could use NavigationView and Navigation Link.
Related
I'm a beginner here, and just trying to put together a simple form and button (right now they don't do anything). For some reason I can't seem to move the form -- which is just going to be one text input by the way, so open to using a different method other than form -- to be centered vertically. Can anyone help me diagnose the problem? Here is my code:
import SwiftUI
struct CreateTopic: View {
var body: some View {
VStack {
Text("Create Topic")
.font(.largeTitle)
Spacer() // <-- Add a Spacer view above the Form view
Form {
TextField("Name", text: $name)
// TextField("Email", text: $email)
// TextField("Phone", text: $phone)
}
.frame(height: 100)
Spacer() // <-- Add a Spacer view below the Form view
NavigationView {
StandardButton(text: "Test", action: {})
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
#State private var name = ""
// #State private var email = ""
// #State private var phone = ""
}
struct CreateTopic_Previews: PreviewProvider {
static var previews: some View {
CreateTopic()
}
}
I tried adding that Spacer() above the Form, which didn't really do anything. It did however move the button.
Should I not be using a Form if I am only looking to use one simple text input?
There are multiple misunderstandings here. We can quickly solve them.
Form is a scrollable form. It is typically meant to encompass the entire screen. And if you use a form, you will not be able to center its content. Forms always lay out their subviews top-to-bottom. You could center a form within a larger view, but I wouldn't. I ran your code and you can scroll the form within the 100pt-tall rectangle. It's just weird.
NavigationView is meant to wrap the entire screen, not just a single button. It does things like add a navigation bar, display a title, and support buttons in the corners of the screen. It also supports the standard in-and-out screen transitions across all of iOS. Think of the Messages app and what happens when you tap into a conversation. NavigationView does that stuff.
With these two clarifications in mind, I would rewrite your view one of two ways.
Option 1, using a Form
struct CreateTopic: View {
var body: some View {
NavigationView {
Form {
Section {
TextField("Name", text: $name)
.textContentType(.name)
TextField("Email", text: $email)
.textContentType(.emailAddress)
TextField("Phone", text: $phone)
.textContentType(.telephoneNumber)
.keyboardType(.phonePad)
}
Section {
Button("Create") {
// Create the topic
}
}
}
.navigationTitle("Create Topic")
}
}
#State private var name = ""
#State private var email = ""
#State private var phone = ""
}
Option 2, without a Form, centering your text fields like you wanted
struct CreateTopic: View {
var body: some View {
NavigationView {
VStack {
TextField("Name", text: $name)
.textContentType(.name)
TextField("Email", text: $email)
.textContentType(.emailAddress)
TextField("Phone", text: $phone)
.textContentType(.telephoneNumber)
.keyboardType(.phonePad)
Button("Create") {
// Create the topic
}
}
.textFieldStyle(.roundedBorder)
.padding()
.frame(maxHeight: .infinity)
.background(Color(.systemGroupedBackground))
.navigationTitle("Create Topic")
}
}
#State private var name = ""
#State private var email = ""
#State private var phone = ""
}
Form fits the style of iOS more, but the second option looks fine and could be what you wanted.
This is the Login() view:
struct Login: View {
#Environment(\.presentationMode) var presentationMode
var body: some View{
VStack{
HStack{
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "xmark")
.resizable()
.frame(width: 18, height: 18)
}
}
NavigationLink(destination: CreateAccount().navigationBarBackButtonHidden(true), label: {
Text("create account")
// and close the current view Login()
})
}
}
}
Is it possible to open a new view, in this case CreateAccount() and closing the current view Login()?
In order to do this, I would suggest skipping the NavigationView altogether, see here for more info. An example for your situation:
//You need an `ObservedObject` to do this, and a overall holder view
enum ViewStates{
//Declare possible views
case ContentView
case Login
case CreateAccount
}
//Then use an observableObject
class viewControl: ObservableObject{
#Published var currentView: ViewStates = .ContentView
}
//Finally, pass this into your views. Take a look at the second part of the tutorial I posted below for more info
//such as using `EnvironmentObject` and adding animation. Example implimentation below:
struct ControllerView: View{
#StateObject var controller: viewControl
var body: some View{
switch controller.currentView{
case .ContentView:
ContentView(controller: controller)
case .Login:
Login(controller: controller)
case .CreateAccount:
CreateAccount(controller: controller)
}
}
}
Next, you need to have #ObservedObject var controller: viewControl in all of your views. Note that you don't need a default statement in the switch clause, because the enum declares all possible values. The following is an example CreateAccount view. You also no longer need the dismiss - in fact, that will no longer work.
struct CreateAccount: View{
#ObservedObject var controller: viewControl
var body: some View{
//Content
Button("Dismiss"){
controller.currentView = .ContentView
}
}
}
This will allow you to switch the view by clicking. Instead of a NavigationLink in ContentView, do this:
Button{
controller.currentView = .CreateAccount
} label: {
Text("Create Account")
}
To go back, you just set the value again. This can also be expanded to show more views.
Second part of the tutorial
I'm using TabView on my home page. Let's just say I have 4 tabs.
On second tab, i can go to another view using NavigationLink and I go to another 2 views using NavigationLink. Then on the latest view, there is a button to present a view and i use .fullScreenCover (since I want to present it full screen).
In the presenting view, I add an X mark on the left side of the navigationBarItems to dismiss. I use #Environment(\.presentationMode) var presentationMode and presentationMode.wrappedValue.dismiss() to dismiss. But it only dismiss the presenting view to the previous view, while actually I want to dismiss it to the root of my view which is the 2nd tab of my TabView.
Is there a way to do this? Because I have looked up to some articles and nothing relevant especially in TabView context.
I also have a question tho:
Is it a right approach to use .fullScreenCover? Or is there another possible solution for example presenting a modal with full screen style (if there's any cause i'm not sure either).
Any suggestions will be very appreciated, thankyou in advance.
The presentationMode is one-level effect value, ie changing it you close one currently presented screen.
Thus to close many presented screens you have to implement this programmatically, like in demo below.
The possible approach is to use custom EnvironmentKey to pass it down view hierarchy w/o tight coupling of every level view (like with binding) and inject/call only at that level where needed.
Demo tested with Xcode 12.4 / iOS 14.4
struct ContentView: View {
var body: some View {
TabView {
Text("Tab1")
.tabItem { Image(systemName: "1.square") }
Tab2RootView()
.tabItem { Image(systemName: "2.square") }
}
}
}
struct Tab2RootView: View {
#State var toRoot = false
var body: some View {
NavigationView {
Tab2NoteView(level: 0)
.id(toRoot) // << reset to root !!
}
.environment(\.rewind, $toRoot) // << inject here !!
}
}
struct Tab2NoteView: View {
#Environment(\.rewind) var rewind
let level: Int
#State private var showFullScreen = false
var body: some View {
VStack {
Text(level == 0 ? "ROOT" : "Level \(level)")
NavigationLink("Go Next", destination: Tab2NoteView(level: level + 1))
Divider()
Button("Full Screen") { showFullScreen.toggle() }
.fullScreenCover(isPresented: $showFullScreen,
onDismiss: { rewind.wrappedValue.toggle() }) {
Tab2FullScreenView()
}
}
}
}
struct RewindKey: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var rewind: Binding<Bool> {
get { self[RewindKey.self] }
set { self[RewindKey.self] = newValue }
}
}
struct Tab2FullScreenView: View {
#Environment(\.presentationMode) var mode
var body: some View {
Button("Close") { mode.wrappedValue.dismiss() }
}
}
You have 2 options:
With .fullScreenCover you will have a binding that results in it being presented you can pass this binding through to the content and when the user taps on x set to to false
You can use the #Environment(\.presentationMode) var presentationMode then call presentationMode.wrappedValue.dismiss() in your button body.
Edit:
If you want to unwind all the way you should make the TabView be binding based. I like to use SceneStorage for this take a look at this post then you can access this SceneStorage value anywhere in your app to respond to it but also to update and change the navigation (this also has the benefit of providing you proper state restoration!)
If you make your TabView in this way:
struct ContentView: View {
#SceneStorage("selectedTab") var selectedTab: Tab = .car
var body: some View {
TabView(selection: $selectedTab) {
CarTrips()
.tabItem {
Image(systemName: "car")
Text("Car Trips")
}.tag(Tab.car)
TramTrips()
.tabItem {
Image(systemName: "tram.fill")
Text("Tram Trips")
}.tag(Tab.tram)
AirplaneTrips()
.tabItem {
Image(systemName: "airplane")
Text("Airplane Trips")
}.tag(Tab.airplaine)
}
}
}
enum Tab: String {
case car
case tram
case airplaine
}
Then deep within your app in the place you want to change the navigation you can create a button view.
struct ViewCarButton: View {
#SceneStorage("selectedTab") var selectedTab: Tab = .car
var body: some View {
Button("A Button") {
selectedTab = .car
}
}
}
This will forced the selected tab to be the car tab.
if instead of this you do not want to change tab but rather change what the navigation view is navigated to you can use the same concept for that, NavigationLink that's a binding if this binding is created using a #SceneStorage then in your ViewCarButton you can make changes to it that will change the navigation state.
This question is identical to SwiftUI #Binding update doesn't refresh view, but the accepted answer is not applicable for my case.
The accepted answer says
A View using a #Binding will update when the underlying #State change, but the #State must be defined within the view hierarchy. (Else you could bind to a publisher)
In my case, the view hierarchy doesn't have the view which is having the #State. The view having the binding is presented modally to the user.
To summarize the issue again
I want to create a view, similar to Toggle which initializes from a Binding. This view will show the contents from the wrapped value and as it performs the updates, the original storage of the value will get updated automatically.
As I have learnt, updating the #Binding in a view, doesn't invalidate it. Then how to implement such a view.
Also I can't depend on the parent view to eventually update this view, because the view is shown on a modally presented screen.
I don't want to use workarounds like using a #State to explicitly trigger a refresh. So what is the correct way to implement such a view.
Code example
The view TextModifier takes a Binding. The view does some modifications to the view. For now it just appends "_Updated" to the value passed.
I initialize the view as TextModifier(text: <some_binding_var>)
struct TextModifier: View {
#Binding var text: String
var body: some View {
Text(text)
.onTapGesture {
text += "_Updated"
}
}
}
This view shows the text and on tapping it updates it in the original source, but as expected the view doesn't update itself on tapping.
So, how to implement this view so that it also updates itself when it updates the binding value.
The accepted answer to the linked question also says
Else you could bind to a publisher
I don't know how to do this. Does anybody know how to implement this and also provide a code example. Thanks.
Updated with full code and gif
struct ContentView: View {
#ObservedObject var viewModel = TestViewModel()
var body: some View {
List {
ForEach(viewModel.itemsList, id: \.self) { item in
ItemView(text: $viewModel.itemsList[getItemIndex(item)])
}
}
}
private func getItemIndex(_ item: String) -> Int {
viewModel.itemsList.firstIndex { $0 == item }!
}
}
class TestViewModel: ObservableObject {
#Published var itemsList = ["Item 1", "Item 2", "Item 3"]
}
struct ItemView: View {
#Binding var text: String
#State private var showEditorView = false
var body: some View {
Text(text)
.onTapGesture {
showEditorView = true
}
.sheet(isPresented: $showEditorView) {
TextModifier(text: $text, showView: $showEditorView)
}
}
}
struct TextModifier: View {
#Binding var text: String
#Binding var showView: Bool
var body: some View {
VStack(spacing: 20) {
Text("Tap on the text to update it")
.foregroundColor(.blue)
Text(text)
.onTapGesture {
text += "_Updated"
}
Button {
showView = false
} label: {
Text("Dismiss")
.foregroundColor(.blue)
}
}
}
}
I want to achieve the function. Like, "Look up" view that is from Apple.
My aim is when the sheet view push another view by navigation, the user can tap the navigation item button to close the sheet view. Like, this below gif.
I try to achieve this function.
I found a problem that is when the user tap the "Done" button. The App doesn't close the sheet view. It only pop the view to parent view. Like, this below gif.
This is my code.
import SwiftUI
struct ContentView: View {
#State var isShowSheet = false
var body: some View {
Button(action: {
self.isShowSheet.toggle()
}) {
Text("Tap to show the sheet")
}.sheet(isPresented: $isShowSheet) {
NavView()
}
}
}
struct NavView: View {
var body: some View {
NavigationView {
NavigationLink(destination: NavSubView()) {
Text("Enter Sub View")
}
} .navigationViewStyle(StackNavigationViewStyle())
}
}
struct NavSubView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}){
Text("Done")
}
)
}
}
How did I achieve this function? :)
Please help me, thank you. :)
UPDATE: Restored original version - provided below changes should be done, as intended, to the topic starter's code. Tested as worked with Xcode 13 / iOS 15
As navigation in sheet might be long enough and closing can be not in all navigation subviews, I prefer to use environment to have ability to specify closing feature only in needed places instead of passing binding via all navigation stack.
Here is possible approach (tested with Xcode 11.2 / iOS 13.2)
Define environment key to store sheet state
struct ShowingSheetKey: EnvironmentKey {
static let defaultValue: Binding<Bool>? = nil
}
extension EnvironmentValues {
var showingSheet: Binding<Bool>? {
get { self[ShowingSheetKey.self] }
set { self[ShowingSheetKey.self] = newValue }
}
}
Set this environment value to root of sheet content, so it will be available in any subview when declared
}.sheet(isPresented: $isShowSheet) {
NavView()
.environment(\.showingSheet, self.$isShowSheet)
}
Declare & use environment value only in subview where it is going to be used
struct NavSubView: View {
#Environment(\.showingSheet) var showingSheet
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button("Done") {
self.showingSheet?.wrappedValue = false
}
)
}
}
I haven't tried SwiftUI ever, but I came from UIKit + RxSwift, so I kinda know how binding works. I read quite a bit of sample codes from a SwiftUI Tutorial, and the way you dismiss a modal is actually correct, but apparently not for a navigation stack.
One way I learned just now is use a #Binding var. This might not be the best solution, but it worked!
So you have this $isShowSheet in your ContentView. Pass that object to your NavView struct by declaring a variable in that NavView.
ContentView
.....
}.sheet(isPresented: $isShowSheet) {
NavView(isShowSheet: self.$isShowSheet)
}
NavView
struct NavView: View {
#Binding var isShowSheet: Bool
var body: some View {
NavigationView {
NavigationLink(destination: NavSubView(isShowSheet: self.$isShowSheet)) {
Text("Enter Sub View")
}
} .navigationViewStyle(StackNavigationViewStyle())
}
}
and finally, do the same thing to your subView.
NavSubView
struct NavSubView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var isShowSheet: Bool
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button(action: {
//self.presentationMode.projectedValue.wrappedValue.dismiss()
self.isShowSheet = false
}){
Text("Done")
}
)
}
}
Now as you can see, you just need to send a new signal to that isShowSheet binding var - false.
self.isShowSheet = false
Voila!
Here's an improved version of Asperi's code from above since they won't accept my edit. Main credit goes to them.
As navigation in sheet might be long enough and closing can be not in all navigation subviews, I prefer to use environment to have ability to specify closing feature only in needed places instead of passing binding via all navigation stack.
Here is possible approach (tested with Xcode 13 / iOS 15)
Define environment key to store sheet state
struct ShowingSheetKey: EnvironmentKey {
static let defaultValue: Binding<Bool>? = nil
}
extension EnvironmentValues {
var isShowingSheet: Binding<Bool>? {
get { self[ShowingSheetKey.self] }
set { self[ShowingSheetKey.self] = newValue }
}
}
Set this environment value to root of sheet content, so it will be available in any subview when declared
#State var isShowingSheet = false
...
Button("open sheet") {
isShowingSheet?.wrappedValue = true
}
// note no $ in front of isShowingSheet
.sheet(isPresented: isShowingSheet ?? .constant(false)) {
NavView()
.environment(\.isShowingSheet, self.$isShowingSheet)
}
Declare & use environment value only in subview where it is going to be used
struct NavSubView: View {
#Environment(\.isShowingSheet) var isShowingSheet
var body: some View {
Text("Hello")
.navigationBarItems(trailing:
Button("Done") {
isShowingSheet?.wrappedValue = false
}
)
}
}