How can I give a swiftUI button multiple functions when pressed? - swift

I created a page for users to register new accounts. I created a "continue" button that is meant to push the new data to firebase and simultaneously move the users to the next view which is my mapView(). Right now the signup fucntion is working, but I cant figure out how to implement the mapView()
Button(action:{
guard !email.isEmpty, !password.isEmpty else {
return
}
viewModel.signUp(email: email, password: password)
} , label: {
Text("Continue")
.foregroundColor(.white)
.frame(width: 350, height: 35)
.background(Color.blue)
.cornerRadius(20)
})
}
Ive tried adding map view inside of the function but Xcode returns a warning that says "Result of 'mapView' initializer is unused".
Button(action:{
guard !email.isEmpty, !password.isEmpty else {
return
}
viewModel.signUp(email: email, password: password)
mapView()
} , label: {
Text("Continue")
.foregroundColor(.white)
.frame(width: 350, height: 35)
.background(Color.blue)
.cornerRadius(20)
})

There are a few ways to pull this off, but using a NavigationStack works really well. Something like this:
//
// ContentView.swift
// animation-example
//
// Created by Andrew Carter on 12/15/22.
//
import SwiftUI
enum SignUpFlow {
case name
case age
case welcome
}
struct SignUpInfo {
var name: String
var age: String
}
struct NameView: View {
#Binding var name: String
var body: some View {
VStack {
TextField("Name", text: $name)
NavigationLink("Next", value: SignUpFlow.age)
.disabled(name.isEmpty)
}
}
}
struct AgeView: View {
#Binding var age: String
var body: some View {
VStack {
TextField("Age", text: $age)
NavigationLink("Next", value: SignUpFlow.welcome)
.disabled(age.isEmpty)
}
}
}
struct WelcomeView: View {
var body: some View {
Text("Welcome")
}
}
struct ContentView: View {
#State private var path: [SignUpFlow] = []
#State private var signUpInfo = SignUpInfo(name: "", age: "")
var body: some View {
NavigationStack(path: $path) {
NavigationLink("Create Account", value: SignUpFlow.name)
.navigationDestination(for: SignUpFlow.self) { flow in
switch flow {
case .age:
AgeView(age: $signUpInfo.age)
case .name:
NameView(name: $signUpInfo.name)
case .welcome:
WelcomeView()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
In the example above with mapView() just placed in the buttons action, it's unused because, well, it's unused. The Button closure expects a Void return, you're just making a map view than instantly throwing it away- it's not used in any way.

Related

Using EnvironmentObject and Binding in same struct? [Beginner]

I have managed to set up a Navigationstack that I use programatically to insert/remove views. In my FindOrderView, I want to carry over the variable "ordernumber" to my LoadingScreen(View) using Binding, but I get an error saying my LoadingScreen does not conform to type 'Equatable'. What could I be doing wrong, and is there any other efficient way to get this working?
I'm a complete beginner with SwiftUi but have searched for hours before posting this here. Thankful for any help.
NavigationRouter (Class used to control layers of views)
import Foundation
import SwiftUI
enum Route: Hashable {
case ContentView
case View1
}
final class NavigationRouter: ObservableObject {
#Published var navigationPath = NavigationPath()
func pushView(route: Route) {
navigationPath.append(route)
}
func popToRootView() {
navigationPath = .init()
}
func popToSpecificView(k: Int) {
navigationPath.removeLast(k)
}
}
Mainapp (App starts here but instantly jumps to MainScreenView())
import SwiftUI
#main
struct AvhamtningApp: App {
#StateObject var router = NavigationRouter()
var body: some Scene {
WindowGroup {
NavigationStack(path: $router.navigationPath) {
MainScreenView()
.navigationDestination(for: Route.self) { route in
switch route {
case .ContentView:
MainScreenView()
case .View1:
FindOrderView()
}
}
}.environmentObject(router)
}
}
}
MainScreenView (View with button called "Get order" that carries you over to View1 (FindOrderView))
import SwiftUI
struct MainScreenView: View {
#EnvironmentObject var router: NavigationRouter
var body: some View {
VStack() {
Text("Testing")
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.yellow)
.padding(.top, 50)
Spacer()
PrimaryButton(text: "Get Order")
.onTapGesture {
router.pushView(route: .View1)
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.white)).ignoresSafeArea(.all)
}
}
struct MainScreenView_Previews: PreviewProvider {
static var previews: some View {
MainScreenView()
}
}
FindOrderView (View with Keypad, if enter 7 digits then should carry you over to Loadingscreen() with the variable "ordernumber" as binding)
import SwiftUI
var list = [["1","2","3"],["4","5","6"],["7","8","9"],["","0","⌫"]]
struct FindOrderView: View {
#State private var ordernumber: String = ""
#EnvironmentObject var router: NavigationRouter
var body: some View {
VStack(spacing: 25) {
Text(ordernumber)
.font(.largeTitle)
.foregroundColor(.black)
.onChange(of: ordernumber) { newValue in
if ordernumber.count >= 7 {
router.navigationPath.append(LoadingScreen(ordernumber: $ordernumber))
}
}
Spacer()
ForEach(list.indices, id: \.self) { listindex in
HStack{
ForEach(list[listindex], id: \.self) { variableindex in
Text(variableindex)
.foregroundColor(.blue)
.font(.system(size: 60))
.onTapGesture(perform: {
if variableindex == "⌫" && !ordernumber.isEmpty {
ordernumber.removeLast()
}
else if ordernumber.count >= 7 {return}
else if variableindex == "⌫" {
()
}
else {
ordernumber += variableindex
}})
.frame(maxWidth: .infinity)
}
}
}
}.background(.white)
}
}
struct FindOrderView_Previews: PreviewProvider {
static var previews: some View {
FindOrderView()
}
}
LoadingScreen (This is where I get the error "Type 'LoadingScreen' does not conform to protocol 'Equatable'")
import SwiftUI
struct LoadingScreen: Hashable, View {
#EnvironmentObject var router: NavigationRouter
#Binding var ordernumber: String
var body: some View {
ZStack{
Text("Loading...")
.font(Font.custom("Baskerville-Bold", size: 50))
.foregroundColor(.orange)
.padding(.bottom, 200)
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .orange))
.scaleEffect(2)
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.white)).ignoresSafeArea(.all)
}
}
struct LoadingScreen_Previews: PreviewProvider {
static var previews: some View {
LoadingScreen(ordernumber: .constant("constant"))
}
}
So the app starts on the MainScreenView because it's set as the root of the NavigationStack.
Your issue with navigation is that you're trying to send the view as a navigation destination value, but you need to send the route value, and switch on the navigationDestination to generate the view to be pushed.
So in your Route enum, add a route for loading:
case loading(orderNumber: String)
Then in your NavigationLink send that new route:
NavigationLink(value: Route.loadingScreen(orderNumber: orderNumber)
Then in your navigationDestination view modifier:
.navigationDestination(for: Route.self) { route in
switch route {
case .ContentView:
MainScreenView()
case .View1:
FindOrderView()
case .loading(let orderNumber):
LoadingScreen(orderNumber: orderNumber)
.environmentObject(router)
}
}
Hope this helps!

SWIFTUI/ How to delay .task and let it work only if the data from textFields are passed

I just tried to make an API app in SwiftUI with loveCalculator from rapidapi.com
The problem is that API first needs names from me before it gives me the results.
My program works but fetching data form API earlier that I want (when I click to show my data in next view, first show default data, then show the data that should be displayed when I click).
Also Is it possible to initialize #Published var loveData (in LoveViewModel) without passing any default data or empty String?
Something like make the data from LoveData optional ?
Can You tell me when I make mistake?
MY CODE IS :
LoveData (for api)
struct LoveData: Codable {
let percentage: String
let result: String
}
LoveViewModel
class LoveViewModel: ObservableObject {
#Published var loveData = LoveData(percentage: "50", result: "aaa")
let baseURL = "https://love-calculator.p.rapidapi.com/getPercentage?"
let myApi = "c6c134a7f0msh980729b528fe273p1f337fjsnd17137cb2f24"
func loveCal (first: String, second: String) async {
let completedurl = "\(baseURL)&rapidapi-key=\(myApi)&sname=\(first)&fname=\(second)"
guard let url = URL(string: completedurl) else {return }
do {
let decoder = JSONDecoder()
let (data, _) = try await URLSession.shared.data(from: url)
if let safeData = try? decoder.decode(LoveData.self, from: data) {
print("succesfully saved data")
self.loveData = safeData
}
} catch {
print(error)
}
}
}
LoveCalculator View
struct LoveCalculator: View {
#State var firstName = ""
#State var secondName = ""
#ObservedObject var loveViewModel = LoveViewModel()
var body: some View {
NavigationView {
ZStack{
Color(.systemTeal).ignoresSafeArea()
VStack(alignment: .center) {
Text("Love Calculator")
.padding(.top, 100)
Text("Enter first name:")
.padding()
TextField("first name", text: $firstName)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
Text("Enter second name:")
.padding()
TextField("second name", text: $secondName, onEditingChanged: { isBegin in
if isBegin == false {
print("finish get names")
}
})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
NavigationLink {
LoveResults(percentage: loveViewModel.loveData.percentage, description: loveViewModel.loveData.result)
} label: {
Text("CHECK IT!")
}
Spacer()
}
}
.task {
await loveViewModel.loveCal(first: firstName, second: secondName)
}
}
}
}
LoveResults View
struct LoveResults: View {
var percentage: String
var description: String
var body: some View {
ZStack{
Color(.green).ignoresSafeArea()
VStack{
Text("RESULTS :")
.padding()
Text(percentage)
.font(.system(size: 80, weight: .heavy))
.foregroundColor(.red)
.padding()
Text(description)
}
}
}
}
Thanks for help!
Regards,
Michal
.task is the same modifier as .onAppear in terms of view lifecycle, meaning it will fire immediately when the view appears. You have to move your API call to a more controlled place, where you can call it once you have all the required data.
If you want to fire the API request only after both names are entered, you can create a computed variable, that checks for desired state of TextFields and when that variable turns to true, then you call the API.
Like in this example:
struct SomeView: View {
#State var firstName: String = ""
#State var secondName: String = ""
var namesAreValid: Bool {
!firstName.isEmpty && !secondName.isEmpty // << validation logic here
}
var body: some View {
Form {
TextField("Enter first name", text: $firstName)
TextField("Enter second name", text: $secondName)
}
.onChange(of: namesAreValid) { isValid in
if isValid {
// Call API
}
}
}
}
You can also set your loveData to optional using #Published var loveData: LoveData? and disable/hide the navigation link, until your data is not nil. In that case, you might need to provide default values with ?? to handle optional string errors in LoveResults view initializer
It was very helpful but still not enough for me, .onChange work even if I was type 1 letter in my Form.
I find out how to use Task { } and .onCommit on my TextField, now everything working well !
My code now looks like that :
LoveCalculator View :
struct LoveCalculator: View {
#State var firstName = ""
#State var secondName = ""
var namesAreValid: Bool {
!firstName.isEmpty && !secondName.isEmpty
}
func makeApiCall() {
if namesAreValid {
DispatchQueue.main.async {
Task {
await self.loveViewModel.loveCal(first: firstName, second: secondName)
}
}
}
}
#ObservedObject var loveViewModel = LoveViewModel()
var body: some View {
NavigationView {
ZStack{
Color(.systemTeal).ignoresSafeArea()
VStack(alignment: .center) {
Text("Love Calculator")
.padding(.top, 100)
Text("Enter first name:")
.padding()
TextField("first name", text: $firstName, onCommit: {makeApiCall()})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
Text("Enter second name:")
.padding()
TextField("second name", text: $secondName, onCommit: {
makeApiCall()
})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
NavigationLink {
LoveResults(percentage: loveViewModel.loveData.percentage ?? "", description: loveViewModel.loveData.result ?? "")
} label: {
Text("CHECK IT!")
}
Spacer()
}
}
}
}
}
Thank You one more time for very helpful tip!
Peace!
Michał ;)
Editing :
Just look at Apple documentation and I see that they say that .onCommit is deprecated whatever it means.
So instead of this I use .onSubmit and works the same !
TextField("second name", text: $secondName)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
.onSubmit {
makeApiCall()
}
Peace! :)

How to setup NavigationLink in SwiftUI sheet to redirect to new view

I am attempting to build a multifaceted openweathermap app. My app is designed to prompt the user to input a city name on a WelcomeView, in order to get weather data for that city. After clicking search, the user is redirected to a sheet with destination: DetailView, which displays weather details about that requested city. My goal is to disable dismissal of the sheet in WelcomeView and instead add a navigationlink to the sheet that redirects to the ContentView. The ContentView in turn is set up to display a list of the user's recent searches (also in the form of navigation links).
My issues are the following:
The navigationLink in the WelcomeView sheet does not work. It appears to be disabled. How can I configure the navigationLink to segue to destination: ContentView() ?
After clicking the navigationLink and redirecting to ContentView, I want to ensure that the city name entered in the WelcomeView textfield is rendered as a list item in the ContentView. For that to work, would it be necessary to set up an action in NavigationLink to call viewModel.fetchWeather(for: cityName)?
Here is my code:
WelcomeView
struct WelcomeView: View {
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
#State private var showingDetail: Bool = false
#State private var linkActive: Bool = true
#State private var acceptedTerms = false
var body: some View {
Section {
HStack {
TextField("Search Weather by City", text: $cityName)
.padding()
.overlay(RoundedRectangle(cornerRadius: 10.0).strokeBorder(Color.gray, style: StrokeStyle(lineWidth: 1.0)))
.padding()
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}
.sheet(isPresented: $showingDetail) {
VStack {
NavigationLink(destination: ContentView()){
Text("Return to Search")
}
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}.interactiveDismissDisabled(!acceptedTerms)
}
}
}.padding()
}
}
}
struct WelcomeView_Previews: PreviewProvider {
static var previews: some View {
WelcomeView()
}
}
ContentView
let coloredToolbarAppearance = UIToolbarAppearance()
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
#State var showingDetail = false
init() {
// toolbar attributes
coloredToolbarAppearance.configureWithOpaqueBackground()
coloredToolbarAppearance.backgroundColor = .systemGray5
UIToolbar.appearance().standardAppearance = coloredToolbarAppearance
UIToolbar.appearance().scrollEdgeAppearance = coloredToolbarAppearance
}
var body: some View {
NavigationView {
VStack() {
List () {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city)) {
HStack {
Text(city.name).font(.system(size: 32))
Spacer()
Text("\(city.main.temp, specifier: "%.0f")°").font(.system(size: 32))
}
}
}.onDelete { index in
self.viewModel.cityNameList.remove(atOffsets: index)
}
}.onAppear() {
viewModel.fetchWeather(for: cityName)
}
}.navigationTitle("Weather")
.toolbar {
ToolbarItem(placement: .bottomBar) {
HStack {
TextField("Enter City Name", text: $cityName)
.frame(minWidth: 100, idealWidth: 150, maxWidth: 240, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading)
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}.sheet(isPresented: $showingDetail) {
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DetailView
struct DetailView: View {
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name)
.font(.system(size: 32))
Text("\(detail.main.temp, specifier: "%.0f")°")
.font(.system(size: 44))
Text(detail.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherModel.init())
}
}
ViewModel
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=<MyAPIKey>") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}
Model
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Double = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
DemoApp
#main
struct SwftUIMVVMWeatherDemoApp: App {
var body: some Scene {
WindowGroup {
// ContentView()
WelcomeView()
}
}
}

List scroll freeze on catalyst NavigationView

I've run in to an odd problem with NavigationView on macCatalyst. Here below is a simple app with a sidebar and a detail view. Selecting an item on the sidebar shows a detail view with a scrollable list.
Everything works fine for the first NavigationLink, the detail view displays and is freely scrollable. However, if I select a list item which triggers a link to a second detail view, scrolling starts, then freezes. The app still works, only the detail view scrolling is locked up.
The same code works fine on an iPad without any freeze. If I build for macOS, the NavigationLink in the detail view is non-functional.
Are there any known workarounds ?
This is what it looks like, after clicking on LinkedView, a short scroll then the view freezes. It is still possible to click on the back button or another item on the sidebar, but the list view is blocked.
Here is the code:
ContentView.swift
import SwiftUI
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
var body: some View {
NavigationView {
List() {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailListView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
}
}
}
struct NamedItem: Identifiable {
let name: String
let id = UUID()
}
struct DetailListView: View {
var item: NamedItem
let sections = (0...4).map({NamedItem(name: "\($0)")})
var body: some View {
VStack {
List {
Text(item.name)
NavigationLink(destination: DetailListView(item: NamedItem(name: "LinkedView"))) {
listItem(" LinkedView", "Item")
.foregroundColor(Color.blue)
}
ForEach(sections) { section in
sectionDetails(section)
}
}
}
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}
TestListApp.swift
import SwiftUI
#main
struct TestListApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
I had this very same problem with Mac Catalyst app. On real device (iPhone 7 with iOS 14.4.2) there was no problem but with Mac Catalyst (MacBook Pro with Big Sur 11.2.3) the scrolling in the navigation view stuck very randomly as you explained. I figured out that the issue was with Macbook's trackpad and was related to scroll indicators because with external mouse the issue was absent. So the easiest solution to this problem is to hide vertical scroll indicators in navigation view. At least it worked for me. Below is some code from root view 'ContentView' how I did it. It's unfortunate to lose scroll indicators with big data but at least the scrolling works.
import SwiftUI
struct TestView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: NewView()) {
Text("Navigation Link to new view")
}
}
.onAppear {
UITableView.appearance().showsVerticalScrollIndicator = false
}
}
}
}
OK, so I managed to find a workaround, so thought I'd post this for help, until what seems to be a macCatalyst SwiftUI bug is fixed. I have posted a radar for the list freeze problem: FB8994665
The workaround is to use NavigationLink only to the first level of the series of pages which can be navigated (which gives me the sidebar and a toolbar), and from that point onwards use the NavigationStack package to mange links to other pages.
I ran in to a couple of other gotcha's with this arrangement.
Firstly the NavigationView toolbar loses its background when scrolling linked list views (unless the window is defocussed and refocussed), which seems to be another catalyst SwiftUI bug. I solved that by setting the toolbar background colour.
Second gotcha was that under macCatalyst the onTouch view modifier used in NavigationStack's PushView label did not work for most single clicks. It would only trigger consistently for double clicks. I fixed that by using a button to replace the label.
Here is the code, no more list freezes !
import SwiftUI
import NavigationStack
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
#State private var isSelected: UUID? = nil
init() {
// Ensure toolbar is allways opaque
UINavigationBar.appearance().backgroundColor = UIColor.secondarySystemBackground
}
var body: some View {
NavigationView {
List {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailStackView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.toolbar { Spacer() }
}
}
}
struct NamedItem: Identifiable {
let name: String
let id = UUID()
}
// Embed the list view in a NavigationStackView
struct DetailStackView: View {
var item: NamedItem
var body: some View {
NavigationStackView {
DetailListView(item: item)
}
}
}
struct DetailListView: View {
var item: NamedItem
let sections = (0...10).map({NamedItem(name: "\($0)")})
var linked = NamedItem(name: "LinkedView")
// Use a Navigation Stack instead of a NavigationLink
#State private var isSelected: UUID? = nil
#EnvironmentObject private var navigationStack: NavigationStack
var body: some View {
List {
Text(item.name)
PushView(destination: linkedDetailView,
tag: linked.id, selection: $isSelected) {
listLinkedItem(" LinkedView", "Item")
}
ForEach(sections) { section in
if section.name != "0" {
sectionDetails(section)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(item.name)
}
// Ensure that the linked view has a toolbar button to return to this view
var linkedDetailView: some View {
DetailListView(item: linked)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
self.navigationStack.pop()
}, label: {
Image(systemName: "chevron.left")
})
}
}
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
// Use a button to select the linked view with a single click
func listLinkedItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Button(title, action: {
self.isSelected = linked.id
})
.foregroundColor(Color.blue)
Text(value)
.padding(.leading, 10)
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}
I have continued to experiment with NavigationStack and have made some modifications which will allow it to swap in and out List rows directly. This avoids the problems I was seeing with the NavigationBar background. The navigation bar is setup at the level above the NavigationStackView and changes to the title are passed via a PreferenceKey. The back button on the navigation bar hides if the stack is empty.
The following code makes use of PR#44 of swiftui-navigation-stack
import SwiftUI
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
#State private var isSelected: UUID? = nil
var body: some View {
NavigationView {
List {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailStackView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.toolbar { Spacer() }
}
}
}
struct NamedItem: Identifiable {
let name: String
let depth: Int
let id = UUID()
init(name:String, depth: Int = 0) {
self.name = name
self.depth = depth
}
var linked: NamedItem {
return NamedItem(name: "Linked \(depth+1)", depth:depth+1)
}
}
// Preference Key to send title back down to DetailStackView
struct ListTitleKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
extension View {
func listTitle(_ title: String) -> some View {
self.preference(key: ListTitleKey.self, value: title)
}
}
// Embed the list view in a NavigationStackView
struct DetailStackView: View {
var item: NamedItem
#ObservedObject var navigationStack = NavigationStack()
#State var toolbarTitle: String = ""
var body: some View {
List {
NavigationStackView(noGroup: true, navigationStack: navigationStack) {
DetailListView(item: item, linked: item.linked)
.listTitle(item.name)
}
}
.listStyle(PlainListStyle())
.animation(nil)
// Updated title
.onPreferenceChange(ListTitleKey.self) { value in
toolbarTitle = value
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(toolbarTitle) \(self.navigationStack.depth)")
.toolbar(content: {
ToolbarItem(id: "BackB", placement: .navigationBarLeading, showsByDefault: self.navigationStack.depth > 0) {
Button(action: {
self.navigationStack.pop()
}, label: {
Image(systemName: "chevron.left")
})
.opacity(self.navigationStack.depth > 0 ? 1.0 : 0.0)
}
})
}
}
struct DetailListView: View {
var item: NamedItem
var linked: NamedItem
let sections = (0...10).map({NamedItem(name: "\($0)")})
// Use a Navigation Stack instead of a NavigationLink
#State private var isSelected: UUID? = nil
#EnvironmentObject private var navigationStack: NavigationStack
var body: some View {
Text(item.name)
PushView(destination: linkedDetailView,
tag: linked.id, selection: $isSelected) {
listLinkedItem(" LinkedView", "Item")
}
ForEach(sections) { section in
if section.name != "0" {
sectionDetails(section)
}
}
}
// Ensure that the linked view has a toolbar button to return to this view
var linkedDetailView: some View {
DetailListView(item: linked, linked: linked.linked)
.listTitle(linked.name)
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
func buttonAction() {
self.isSelected = linked.id
}
// Use a button to select the linked view with a single click
func listLinkedItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Button(title, action: buttonAction)
.foregroundColor(Color.blue)
Text(value)
.padding(.leading, 10)
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}

Swift UI button action function

I completely new on Swift, so excuse possible lack of precision. I am trying to build a login form that authenticates with a REST API.
Here is what I have so far following this tutorial.
My next step is understanding where to put all of the logic for submitting the form. I would like to extract it from the inline manner it is taking.
Can I pass a function to the action parameter? I tried finding some sort of extract feature on Xcode but couldn't get it to work (they are greyed).
import SwiftUI
let lightGreyColor = Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0)
let storedUsername = "john"
let storedPassword = "1234"
struct ContentView: View {
#State var username: String = ""
#State var password: String = ""
#State var authenticationDidFail: Bool = false
#State var authenticationDidSucceed: Bool = false
var body: some View {
ZStack {
VStack {
Image("logo")
EmailField(username: $username)
PasswordField(password: $password)
if authenticationDidFail {
Text("Information not correct. Try again.")
.offset(y: -10)
.foregroundColor(.red)
}
Button(action: {
if self.password == storedPassword {
print(password)
self.authenticationDidSucceed = true
self.authenticationDidFail = false
} else {
self.authenticationDidFail = true
}
}) {
LoginButtonContent()
}
}
if authenticationDidSucceed {
Text("Login succeeded!")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct LoginButtonContent: View {
var body: some View {
Text("LOGIN")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(width: 220, height: 60)
.background(Color.green)
.cornerRadius(15.0)
}
}
struct PasswordField: View {
#Binding var password: String
var body: some View {
SecureField("Password", text: $password)
.padding()
.background(lightGreyColor)
.cornerRadius(5.0)
.padding(.bottom, 20)
}
}
struct EmailField: View {
#Binding var username: String
var body: some View {
TextField("Username", text: $username)
.padding()
.cornerRadius(5.0)
.padding(.bottom, 20)
}
}
You can use an Object of your Model inside your Button's action, there you can execute your REST call and return a result to set a value to authenticationDidSucceed, so that the UI is updated.
If the used class conforms to the ObservableObject protocol you can even use its published variables to automatically update the UI.