How to pass user id to view with firebase - swift

I am working on a social media app and I am having trouble displaying a Profile of a specific user. I am able to do it with currentUser?.uid but I don't know how to pass a different id/user to a profile view. Sorry if this explanation is confusing, I'm also having a hard time putting it into words.
This is my view that fetches the currently logged in user and displays their username:
import SwiftUI
class TestProfileViewModel: ObservableObject {
#Published var qUser: User?
init() {
fetchCurrentUser()
}
func fetchCurrentUser() {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else { return }
FirebaseManager.shared.firestore.collection("users").document(uid).getDocument { snapshot, err in
if let err = err {
print("\(err)")
return
}
guard let data = snapshot?.data() else { return }
print(data)
self.qUser = .init(data: data)
}
}
}
struct TestProfileView: View {
#ObservedObject var vm = TestProfileViewModel()
var body: some View {
Text(vm.qUser?.username ?? "No User")
}
}
struct TestProfileView_Previews: PreviewProvider {
static var previews: some View {
TestProfileView()
}
}
This is the view, where I want to use the ID to fetch a user from my database and use it for a Profile View, ike how I did with the currentUser:
import SwiftUI
struct TestSongVIew: View {
let testUsername = "John"
let testUserID = "123123"
#State var showingUserProfile = false
var body: some View {
VStack {
Button("Open \(testUsername)'s Profile") {
}
}
.fullScreenCover(isPresented: $showingUserProfile, onDismiss: nil) {
TestProfileView()
}
}
}
struct TestSongVIew_Previews: PreviewProvider {
static var previews: some View {
TestSongVIew()
}
}
Here is my FirebaseManager code
import Foundation
import Firebase
import FirebaseFirestore
class FirebaseManager: NSObject {
let auth: Auth
let storage: Storage
let firestore: Firestore
static let shared = FirebaseManager()
override init() {
FirebaseApp.configure()
self.auth = Auth.auth()
self.storage = Storage.storage()
self.firestore = Firestore.firestore()
super .init()
}
}

There are a couple of ways to achieve this, and they all depend on how you set up the navigation for your app.
I'm currently working on a blog post / video to demonstrate how to monitor authentication state in a SwiftUI app. To demonstrate how to implement your use case, I added a profile screen that you can use in two ways:
You can navigate to the profile screen from the app's settings screen. This will show the user profile of the currently signed in user.
You can navigate to the profile screen from a List view showing all user profiles in your user profile collection in Firestore. This might be useful if you want to implement a high score screen that allows the user to navigate to the profile screen for each of the top 10 players in a game.
Ok, here goes:
Profile
The profile model
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
struct Profile: Identifiable, Codable {
#DocumentID var id: String? = ""
var nickname: String
}
extension Profile {
static let empty = Profile(nickname: "")
}
The profile view model
import Foundation
import Combine
import FirebaseFirestore
import FirebaseFirestoreSwift
class ProfileViewModel: ObservableObject {
// MARK: - Output
#Published var profile: Profile
init(profile: Profile) {
self.profile = profile
}
init(uid: String) {
self.profile = Profile.empty
fetchProfile(uid)
}
// MARK: - Private attributes
private var db = Firestore.firestore()
func fetchProfile(_ uid: String) {
db.collection("profiles")
.whereField("uid", isEqualTo: uid)
.getDocuments { querySnapshot, error in
if let error = error {
print("Error getting documents: \(error)")
}
else {
if let querySnapshot = querySnapshot {
if let document = querySnapshot.documents.first {
do {
self.profile = try document.data(as: Profile.self)
}
catch {
}
}
}
}
}
}
}
The profile view
struct ProfileView: View {
#ObservedObject var viewModel: ProfileViewModel
init(profile: Profile) {
self.viewModel = ProfileViewModel(profile: profile)
}
init(uid: String) {
self.viewModel = ProfileViewModel(uid: uid)
}
var body: some View {
Form {
Text(viewModel.profile.nickname)
}
.navigationTitle("Details")
}
}
struct ProfileView_Previews: PreviewProvider {
static var previews: some View {
ProfileView(profile: Profile(nickname: "freter"))
}
}
Settings
The settings view model
import Foundation
import Combine
import FirebaseAuth
class SettingsViewModel: ObservableObject {
// MARK: - Output
#Published var email: String = ""
#Published var idToken: String = ""
#Published var user: User?
#Published var authenticationState: AuthenticationState = .unauthenticated
// MARK: - Dependencies
private var authenticationService: AuthenticationService?
func connect(authenticationService: AuthenticationService) {
if self.authenticationService == nil {
self.authenticationService = authenticationService
self.authenticationService?
.$authenticationState
.assign(to: &$authenticationState)
self.authenticationService?
.$user
.assign(to: &$user)
$user
.map { $0?.email }
.replaceNil(with: "(no email address)")
.assign(to: &$email)
}
}
#MainActor
func refreshIDToken() {
Task {
do {
idToken = try await user?.idTokenForcingRefresh(true) ?? ""
}
catch {
idToken = error.localizedDescription
print(error)
}
}
}
}
The settings view
import SwiftUI
struct SettingsView: View {
#StateObject var viewModel = SettingsViewModel()
#Environment(\.dismiss) var dismiss
#EnvironmentObject var authenticationService: AuthenticationService
#State private var presentingLoginScreen = false
var loginButton: some View {
Button(authenticationService.authenticationState == .unauthenticated ? "Login" : "Logout") {
if authenticationService.authenticationState == .unauthenticated {
presentingLoginScreen.toggle()
}
else {
authenticationService.signOut()
}
}
.frame(maxWidth: .infinity)
}
var body: some View {
Form {
Section {
Label("Help & Feedback", systemImage: "questionmark.circle")
Label("About", systemImage: "info.circle")
}
Section {
Label(viewModel.email, systemImage: "at")
Label(viewModel.idToken, systemImage: "person")
Button(action: viewModel.refreshIDToken) {
Text("Refresh ID token")
}
NavigationLink(destination: ProfileView(uid: viewModel.user?.uid ?? "unknown")) {
Label("Show user profile", systemImage: "person")
}
} header: {
Text("User Details")
}
Section {
loginButton
}
}
.sheet(isPresented: $presentingLoginScreen) {
LoginView()
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
dismiss()
}
}
}
.onAppear {
viewModel.connect(authenticationService: authenticationService)
}
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
SettingsView()
.environmentObject(AuthenticationService())
}
}
}
Authentication
The authentication service
import Foundation
import FirebaseAuth
enum AuthenticationState {
case unauthenticated
case authenticating
case authenticated
}
class AuthenticationService: ObservableObject {
// MARK: - Output
#Published var authenticationState: AuthenticationState = .unauthenticated
#Published var errorMessage: String = ""
#Published var user: User?
init() {
registerAuthStateListener()
}
#MainActor
func signIn(withEmail email: String, password: String) async -> Bool {
authenticationState = .authenticating
do {
try await Auth.auth().signIn(withEmail: email, password: password)
return true
}
catch {
await MainActor.run {
errorMessage = error.localizedDescription
authenticationState = .unauthenticated
}
print(error)
return false
}
}
func signOut() {
do {
try Auth.auth().signOut()
}
catch {
print(error)
}
}
private var handle: AuthStateDidChangeListenerHandle?
private func registerAuthStateListener() {
if handle == nil {
handle = Auth.auth().addStateDidChangeListener { auth, user in
Task {
await MainActor.run {
self.user = user
if let user = user {
self.authenticationState = .authenticated
print("User \(user.uid) signed in. Email: \(user.email ?? "(no email address set)"), anonymous: \(user.isAnonymous)")
}
else {
self.authenticationState = .unauthenticated
print("User signed out.")
}
}
}
}
}
}
}
The login view model
import Foundation
import Combine
import FirebaseAuth
class LoginViewModel: ObservableObject {
// MARK: - Input
#Published var email: String = ""
#Published var password: String = ""
// MARK: - Output
#Published var isValid: Bool = false
#Published var authenticationState: AuthenticationState = .unauthenticated
#Published var errorMessage: String = ""
#Published var user: User?
// MARK: - Dependencies
private var authenticationService: AuthenticationService?
func connect(authenticationService: AuthenticationService) {
if self.authenticationService == nil {
self.authenticationService = authenticationService
self.authenticationService?
.$authenticationState
.assign(to: &$authenticationState)
self.authenticationService?
.$errorMessage
.assign(to: &$errorMessage)
self.authenticationService?
.$user
.assign(to: &$user)
Publishers.CombineLatest($email, $password)
.map { !($0.isEmpty && $1.isEmpty) }
.print()
.assign(to: &$isValid)
}
}
func signInWithEmailPassword() async -> Bool {
if let authenticationService = authenticationService {
return await authenticationService.signIn(withEmail: email, password: password)
}
else {
return false
}
}
}
The login view
import SwiftUI
enum FocusableField: Hashable {
case email
case password
}
struct LoginView: View {
#StateObject var viewModel = LoginViewModel()
#EnvironmentObject var authenticationService: AuthenticationService
#Environment(\.dismiss) var dismiss
#FocusState private var focus: FocusableField?
private func signInWithEmailPassword() {
Task {
if await viewModel.signInWithEmailPassword() == true {
dismiss()
}
}
}
var body: some View {
VStack {
Image("Login")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(minHeight: 0)
Text("Login")
.font(.largeTitle)
.fontWeight(.bold)
.frame(maxWidth: .infinity, alignment: .leading)
HStack {
Image(systemName: "at")
TextField("Email", text: $viewModel.email)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.focused($focus, equals: .email)
.submitLabel(.next)
.onSubmit {
self.focus = .password
}
}
.padding(.vertical, 6)
.background(Divider(), alignment: .bottom)
.padding(.bottom, 4)
HStack {
Image(systemName: "lock")
SecureField("Password", text: $viewModel.password)
.focused($focus, equals: .password)
.submitLabel(.go)
.onSubmit {
signInWithEmailPassword()
}
}
.padding(.vertical, 6)
.background(Divider(), alignment: .bottom)
.padding(.bottom, 8)
if !viewModel.errorMessage.isEmpty {
VStack {
Text(viewModel.errorMessage)
.foregroundColor(Color(UIColor.systemRed))
}
}
Button(action: signInWithEmailPassword) {
if viewModel.authenticationState != .authenticating {
Text("Login")
.frame(maxWidth: .infinity)
}
else {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.frame(maxWidth: .infinity)
}
}
.disabled(!viewModel.isValid)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
.controlSize(.large)
HStack {
VStack { Divider() }
Text("or")
VStack { Divider() }
}
Button(action: { }) {
Image(systemName: "applelogo")
.frame(maxWidth: .infinity)
}
.foregroundColor(.black)
.buttonStyle(.bordered)
.controlSize(.large)
HStack {
Text("Don't have an account yet?")
Button(action: {}) {
Text("Sign up")
.fontWeight(.semibold)
.foregroundColor(.blue)
}
}
.padding([.top, .bottom], 50)
}
.onAppear {
viewModel.connect(authenticationService: authenticationService)
}
.listStyle(.plain)
.padding()
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
Group {
LoginView()
.environmentObject(AuthenticationService())
LoginView()
.preferredColorScheme(.dark)
.environmentObject(AuthenticationService())
}
}
}

Related

NavigationLink In NavigationStack is disabled

I wrote test code for NavigationStack. The behavior of the code is a two-step transition(ContentView -> SubsubTestView -> DetailView).
But I got an error when I have selected a name in SubsubTestView.
A NavigationLink is presenting a value of type “User” but there is no matching navigationDestination declaration visible from the location of the link. The link cannot be activated.
Is there anything wrong with the wrong part of the code?
ContentView.swift
import SwiftUI
class EnvObj: ObservableObject {
#Published var users = [User(name: "a"), User(name: "b"), User(name: "c")]
}
struct User: Hashable, Identifiable, Equatable {
var id = UUID()
var name = ""
static func == (lhs: User, rhs: User) -> Bool{
return lhs.id == rhs.id
}
}
struct ContentView: View {
#EnvironmentObject var envObj: EnvObj
#State var moveToSubsub = false
var body: some View {
NavigationStack {
Button("To subsub") {
moveToSubsub = true
}
.navigationDestination(isPresented: $moveToSubsub) {
SubsubTestView(vm: VM(envObj: envObj))
}
}
}
}
struct SubsubTestView: View {
#EnvironmentObject var envObj: EnvObj
#StateObject var vm: VM
var body: some View {
VStack {
List(self.vm.envObj.users) { user in
NavigationLink(value: user) {
Text(user.name)
}
}
.navigationDestination(for: User.self) { user in
DetailView(vm: VMD(envObj: envObj, selectedUser: user))
}
}
}
}
class VM: ObservableObject {
var envObj: EnvObj = .init()
init(envObj: EnvObj) {
self.envObj = envObj
}
}
struct DetailView: View {
#StateObject var vm: VMD
var body: some View {
VStack {
TextField("Name: ", text: (self.$vm.selectedUser ?? User()).name)
Text(self.vm.selectedUser?.name ?? User().name)
Button("submit", action: self.vm.submit)
}
}
}
class VMD: ObservableObject {
var envObj: EnvObj = .init()
#Published var selectedUser: User?
init(envObj: EnvObj, selectedUser: User? = nil) {
self.envObj = envObj
self.selectedUser = selectedUser
}
private(set) lazy var submit = {
if let user = self.selectedUser {
self.update(user: user)
}
}
func update(user: User) {
self.envObj.users = self.envObj.users.map {
return $0 == user ? user : $0
}
}
}
func ??<T>(binding: Binding<T?>, fallback: T) -> Binding<T> {
return Binding(get: {
binding.wrappedValue ?? fallback
}, set: {
binding.wrappedValue = $0
})
}
Thanks,

KVO publisher printing recursively in debugger

Why does the following LoginDateMonitorView result in hundreds of messages in the debugger?
2022-10-13 06:31:03.959864+0100 Test[4412:126282] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
protocol PropertyObserver: ObservableObject {
associatedtype T: NSObject
associatedtype Value
var object: T { get }
var property: KeyPath<T, Value> { get }
}
extension PropertyObserver {
var objectWillChange: AnyPublisher<Value, Never> {
object.publisher(for: property).eraseToAnyPublisher()
}
}
final class User: NSObject {
#objc dynamic var lastLogin: Date = Date(timeIntervalSince1970: 9)
}
final class UserLoginMonitor: ObservableObject {
let user: User
init(_ user: User) {
self.user = user
}
}
extension UserLoginMonitor: PropertyObserver {
var object: User {
user
}
var property: KeyPath<User, Date> {
\.lastLogin
}
}
struct LoginDateMonitorView: View {
#StateObject private var monitor: UserLoginMonitor
init(user: User) {
_monitor = StateObject(wrappedValue: UserLoginMonitor(user))
}
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}

Can I pass a Binding variable as a parameter in a function? (do I need to in this case?)

I'm new to Swift / Firebase and need some help with my code.
I have a simple create new account page, and I am using Firebase.
I want to create a user when a button is tapped - change button colour/text to reflect this - then return to HomeView
Now, I want to include all the Auth.auth() in a separate function on a separate swift file - to keep my View code clean.
Currently, I'm setting the isShowingNewAccountView = false to return back to HomeView (via a NavigationLink on HomeView) - isShowingAccountView is a Binding variable from HomeView.
If I put all the Auth.auth() code into a separate function, is it possible to change the value of the isShowingAccountView to 'false' when a user is created from within the function? Is there a more elegant alternative
Button(action: {
Auth.auth().createUser(withEmail: newUser.userEmail, password: newUser.userPassword) { authDataResult, error in
if error != nil {
print("Error detected")
print(error!.localizedDescription)
isShowingNewAccountView = true //Binding variable from HomeView - Keep showing NewAccountView
newUser.successNewAccountCreated = false
return
}
else
{
print("Account Successfully Created")
isShowingNewAccountView = false //Binding variable from HomeView - Revert back to HomeView
newUser.successNewAccountCreated = true
return
}
}
}, label: {
CustomButton(buttonText: (!newUser.successNewAccountCreated ? "Create New Account" : "New Account Created"), colourVar: newUser.successNewAccountCreated ? .green : (isInputAppropriate() ? .accentColor : .gray)
})
On HomeView I have:
NavigationLink(destination: NewAccountView(isShowingNewAccountView: $isShowingNewAccountView), isActive: $isShowingNewAccountView) {
//passing isShowingAccountView as a binding $
EmptyView()
}
Thanks in advance.
Maybe this approach may help you:
Three views (just as an example):
ContentView: It shows either Page1 or Page2, depending on the var isShowingNewAccountView.
Page1: It is the page you use to create a new user, it will be shown when isShowingNewAccountView is false. It calls the function signUp from the AuthenticationViewModel.
Page2: It is the page that will be opened when a new User has been created and isShowingNewAccountView is true
One ViewModel (also just as an example)
The authentication is handled in the AuthenticationViewModel. It is handling the functions to signUp, signIn and signOut. It also holds isShowingNewAccountView.
Views
import SwiftUI
import FirebaseFirestore
import Firebase
import FirebaseFirestoreSwift
struct Page1: View {
#EnvironmentObject var authenticationViewModel: AuthenticationViewModel
#State private var buttonPressed = false
#State private var email: String = "sebastian#hello.me"
#State private var password: String = "password"
var body: some View {
VStack(){
Text("Register")
.font(Font.system(size: 24, weight: .bold))
.padding()
.padding(.vertical, 50)
TextField("E-Mail", text: $email)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
SecureField("Password", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button(action: {
buttonPressed.toggle()
authenticationViewModel.signUp(email: email, password: password){ success, uid in
if success {
print("UID: \(uid)")
} else {
print("Uh-oh")
}
buttonPressed = false
}
}) {
Text(buttonPressed ? "In Progress" : "Sign Up - Create User")
.foregroundColor(buttonPressed ? Color.pink : Color.cyan)
}
}
}
}
struct Page2: View {
#EnvironmentObject var authenticationViewModel: AuthenticationViewModel
var body: some View {
Button(action: {
authenticationViewModel.isShowingNewAccountView.toggle()
}) {
Text("Back to other page")
}
}
}
struct ContentView: View {
#EnvironmentObject var authenticationViewModel: AuthenticationViewModel
var body: some View {
if authenticationViewModel.isShowingNewAccountView {
Page2()
} else {
Page1()
}
}
}
ViewModel for Authentication
import Foundation
import FirebaseFirestore
import Firebase
import FirebaseFirestoreSwift
class AuthenticationViewModel: ObservableObject {
let db = Firestore.firestore()
#Published var isShowingNewAccountView: Bool = false
// Sign Up
func signUp(email: String, password: String, completion: #escaping (Bool, String)->Void) {
Auth.auth().createUser(withEmail: email, password: password) { authResult, error in
// ERROR AND SUCCESS HANDLING
if error != nil {
// ERROR HANDLING
print(error?.localizedDescription as Any)
completion(false, "ERROR")
} else {
// SUCCESS HANDLING
self.isShowingNewAccountView = true
completion(true, authResult?.user.uid ?? "")
}
}
}
// Sign In
func signIn(email: String, password: String, completion: #escaping (Bool, String)->Void) {
Auth.auth().signIn(withEmail: email, password: password) { (authResult, error) in
// ERROR AND SUCCESS HANDLING
if error != nil {
// ERROR HANDLING
print(error?.localizedDescription as Any)
completion(false, "ERROR")
}
completion(true, authResult?.user.uid ?? "")
}
}
// Sign Out
func signOut(_ completion: #escaping (Bool) ->Void) {
try! Auth.auth().signOut()
completion(true)
}
// Create new user
func createNewUser(name: String, id: String) {
do {
let newUser = User(name: name, id: id)
try db.collection("users").document(newUser.id!).setData(from: newUser) { _ in
print("User \(name) created")
}
} catch let error {
print("Error writing user to Firestore: \(error)")
}
}
}
Alternative with a NavigationLink
In your approach you are using a NavigationLink. That would work as well. In that case the ContentView as well as the Page1 view are slightly different (Page2 stays as it is):
ContentView Update
In the ContentView it is not necessary do make the decision which view will be shown.
struct ContentView: View {
#EnvironmentObject var authenticationViewModel: AuthenticationViewModel
var body: some View {
Page1()
}
}
Page1 Update
On Page1 we now need the NavigationView and the NavigationLink. As soon as isShowingNewAccountView becomes true, it opens Page2, but for me, the NavigationLink is something you can press, like a button. But anyway:
struct Page1: View {
#EnvironmentObject var authenticationViewModel: AuthenticationViewModel
#State private var buttonPressed = false
#State private var email: String = "sebastian#hello.me"
#State private var password: String = "password"
var body: some View {
NavigationView(){
VStack(){
Text("Register")
.font(Font.system(size: 24, weight: .bold))
.padding()
.padding(.vertical, 50)
TextField("E-Mail", text: $email)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
SecureField("Password", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button(action: {
buttonPressed.toggle()
authenticationViewModel.signUp(email: email, password: password){ success, uid in
if success {
print("UID: \(uid)")
} else {
print("Uh-oh")
}
buttonPressed = false
}
}) {
Text(buttonPressed ? "In Progress" : "Sign Up - Create User")
.foregroundColor(buttonPressed ? Color.pink : Color.cyan)
}
NavigationLink(destination: Page2(), isActive: $authenticationViewModel.isShowingNewAccountView){
EmptyView()}
}
}
}
}
Please see the gif:

SwiftUI Modifying state during view update, this will cause undefined behavior - Proper use explanation

Im learning swift and this error/warning is driving me crazy because I cant see what call Im making that causing it... The Xcode warning only shows up in my #main struct
Modifying state during view update, this will cause undefined behavior.
I thought it might be in the ListView, but I realized the warning only shows after the "Submit Post" button is it.
Im looking for a fix, but more importantly and explanation as to why this is happening and the proper usage moving forward.
import SwiftUI
import Firebase
#main
struct SocialcademyApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
PostsList()
}
}
}
struct PostsList: View {
#StateObject var viewModel = PostsViewModel()
#State private var searchText = ""
#State private var showNewPostForm = false
var body: some View {
NavigationView {
List(viewModel.posts) { post in
if searchText.isEmpty || post.contains(searchText) {
PostRow(post: post)
}
}
.searchable(text: $searchText)
.navigationTitle("Posts")
.toolbar {
Button {
showNewPostForm = true
} label: {
Label("New Post", systemImage: "square.and.pencil")
}
}
.sheet(isPresented: $showNewPostForm) {
NewPostView(creationAction: viewModel.makeCreationAction())
}
}
}
}
struct NewPostView: View {
typealias CreationAction = (Post) async throws -> Void
let creationAction: CreationAction
#State private var post = Post(title: "", content: "", authorName: "")
#State private var state = FormState.idle
#Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
Form {
Section {
TextField("Title", text: $post.title)
TextField("Author Name", text: $post.authorName)
}
Section {
TextField("Content", text: $post.content)
.multilineTextAlignment(.leading)
}
Button(action: createPost, label: {
if state == .working {
ProgressView() } else {
Text("Submit Post")
}
})
.font(.headline)
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.padding()
.listRowBackground(Color.accentColor)
}
}
.navigationTitle("New Post")
.disabled(state == .working)
.alert("Cannot Create Post", isPresented: $state.isError, actions: {}) {
Text("Sorry, something went wrong")
}
.onSubmit {
createPost()
}
}
private func createPost() {
print("[NewPostForm] creating a new post")
Task {
state = .working
do {
try await creationAction(post)
dismiss()
} catch {
state = .error
print("[NewPostForm] Cannot create post: \(error)")
}
}
}
}
private extension NewPostView {
enum FormState {
case idle, working, error
var isError: Bool {
get {
self == .error
}
set {
guard !newValue else { return }
self = .idle
}
}
}
}
#MainActor
class PostsViewModel: ObservableObject {
#Published var posts = [Post.testPost]
func makeCreationAction() -> NewPostView.CreationAction {
return { [weak self] post in
try await PostsRepository.create(post)
self?.posts.insert(post, at: 0)
}
}
}

SwiftUI conditional doesn't work at top level in view

I have a view model that looks like this:
class SegmentViewModel: ObservableObject {
#Published private(set) var itemIds = [Int]()
private var cancellables: Set<AnyCancellable> = []
init() {
fetchIds()
}
private func fetchIds() {
let request = URLRequest(path: "/ids")
network
.send(request)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .failure(let error): print(error.localizedDescription)
case .finished: break
}
} receiveValue: { response in
self.itemIds = response
}
.store(in: &cancellables)
}
}
That all works fine and then my View is as follows:
struct SegmentView: View {
#ObservedObject var viewModel: SegmentViewModel
var body: some View {
if viewModel.itemIds.isEmpty {
ProgressView()
} else {
ScrollView {
LazyVStack {
ForEach(viewModel.itemIds, id: \.self) { id in
ItemView(viewModel: ItemViewModel(itemId: id))
.cornerRadius(15)
.padding()
.shadow(radius: 3)
}
}
}
}
}
}
But this doesn't work. The ProgressView is never hidden and replaced with the ScrollView. However, if I change my view to:
struct SegmentView: View {
#ObservedObject var viewModel: SegmentViewModel
var body: some View {
ScrollView {
if viewModel.itemIds.isEmpty {
ProgressView()
} else {
LazyVStack {
ForEach(viewModel.itemIds, id: \.self) { id in
ItemView(viewModel: ItemViewModel(itemId: id))
.cornerRadius(15)
.padding()
.shadow(radius: 3)
}
}
}
}
}
}
Then it works as expected and hides the ProgressView once the itemIds aren't empty.
Why is this?
It looks like ViewBuidler consumed the condition and so it is not observed. I would wrap top at Group
struct SegmentView: View {
#ObservedObject var viewModel: SegmentViewModel
var body: some View {
Group { // << here !!
if viewModel.itemIds.isEmpty {
ProgressView()
} else {
ScrollView {
LazyVStack {
ForEach(viewModel.itemIds, id: \.self) { id in
ItemView(viewModel: ItemViewModel(itemId: id))
.cornerRadius(15)
.padding()
.shadow(radius: 3)
}
}
}
}
}
}
}