I would like to show a ProgressView when the Button is pressed and the user.showLoginProgress property is set, which is declared as published. However, the new value of the property does not seem to trigger the LoginView to refresh, so the ProgressView does not show up:
struct LoginView: View {
#EnvironmentObject var user : User
var body: some View {
VStack{
Button("Login", action: {
user.credentials.showLoginProgress=true // #Published
})
if user.credentials.showLoginProgress{
ProgressView() // never shows up
}
}
}
}
The user is global:
var user : User = User()
#main
struct app1App: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
MainView().environmentObject(user)
}
}
}
and is defined as:
class User : ObservableObject{
#Published var credentials = Login()
}
while the credentials is:
class Login : ObservableObject {
#Published var showLoginProgress : Bool = false
}
Could anyone point out what's wrong with my code?
Try using something like this approach, to re-structure your User and Login, where
there is only one ObservableObject and the login functions are in it.
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject var manager = UserManager()
var body: some View {
LoginView().environmentObject(manager)
}
}
struct LoginView: View {
#EnvironmentObject var manager: UserManager
var body: some View {
VStack {
if manager.user.isAuthorised {
Text(manager.user.name + " is logged in")
}
Button("Login", action: {
manager.doLogin()
})
if manager.showLoginProgress {
ProgressView()
}
}
}
}
class UserManager: ObservableObject {
#Published var user = User()
#Published var showLoginProgress = false
#Published var someOtherVar = ""
func doLogin() {
showLoginProgress = true
// ...
// simulated login
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.user.name = "Mickey Mouse"
self.user.isAuthorised = true
self.showLoginProgress = false
}
}
}
struct User: Identifiable, Hashable {
let id = UUID()
var name = ""
var isAuthorised = false
// var credentials: Credentials
// ....
}
Related
How to observe property value in SwiftUI.
I know some basic publisher and observer patterns. But here is a scenario i am not able to implement.
class ScanedDevice: NSObject, Identifiable {
//some variables
var currentStatusText: String = "Pending"
}
here CurrentStatusText is changed by some other callback method that update the status.
Here there is Model class i am using
class SampleModel: ObservableObject{
#Published var devicesToUpdated : [ScanedDevice] = []
}
swiftui component:
struct ReviewView: View {
#ObservedObject var model: SampleModel
var body: some View {
ForEach(model.devicesToUpdated){ device in
Text(device.currentStatusText)
}
}
}
Here in UI I want to see the real-time status
I tried using publisher inside ScanDevice class but sure can to use it in 2 layer
You can observe your class ScanedDevice, however you need to manually use a objectWillChange.send(),
to action the observable change, as shown in this example code.
class ScanedDevice: NSObject, Identifiable {
var name: String = "some name"
var currentStatusText: String = "Pending"
init(name: String) {
self.name = name
}
}
class SampleViewModel: ObservableObject{
#Published var devicesToUpdated: [ScanedDevice] = []
}
struct ReviewView: View {
#ObservedObject var viewmodel: SampleViewModel
var body: some View {
VStack (spacing: 33) {
ForEach(viewmodel.devicesToUpdated){ device in
HStack {
Text(device.name)
Text(device.currentStatusText).foregroundColor(.red)
}
Button("Change \(device.name)") {
viewmodel.objectWillChange.send() // <--- here
device.currentStatusText = UUID().uuidString
}.buttonStyle(.bordered)
}
}
}
}
struct ContentView: View {
#StateObject var viewmodel = SampleViewModel()
var body: some View {
ReviewView(viewmodel: viewmodel)
.onAppear {
viewmodel.devicesToUpdated = [ScanedDevice(name: "device-1"), ScanedDevice(name: "device-2")]
}
}
}
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,
I have two views, embedded in TabView.
I am using userdefaults in a class called usersettings.
class UserSettings: ObservableObject {
#Published var favList: [String] {
willSet {
print("willset")
}
didSet {
UserDefaults.standard.set(favList, forKey: "isAccountPrivate")
print("didset")
}
}
init() {
self.favList = UserDefaults.standard.object(forKey: "isAccountPrivate") as? [String] ?? ["Sepetiniz Boş"]
}
}
In Button View, which acts like add/remove favorite. It successfully adds and remove from the UserDefaults. But when I add something it does not show on the other view (please see the next code after FavButton)
struct FavButton: View {
#Binding var passedFood: String
#ObservedObject var userSettings = UserSettings()
var body: some View {
Button(action: {
if userSettings.favList.contains(passedFood) {
userSettings.favList.remove(at: userSettings.favList.firstIndex(of: passedFood )!)
} else {
userSettings.favList.append(passedFood)
}
})
}
}
But it does not update my list in this other view unless I close and open my app. If I remove something from the list, it actually removes from the userdefault.
If I add a new word within this view, it works too.
My only problem is when I add something from another view (FavButton) it does not show in this view (FavView).
struct FavView: View {
#ObservedObject var userSettings = UserSettings()
#State private var newWord = ""
var body: some View {
NavigationView {
List {
TextField("Ürün Ekleyin...", text: $newWord, onCommit: addNewWord)
ForEach( self.userSettings.favList, id: \.self) { list in
Text(list)
.font(.headline)
.padding()
}
.onDelete(perform: self.deleteRow)
}
.navigationTitle("Sepetim")
}
}
private func deleteRow(at indexSet: IndexSet) {
self.userSettings.favList.remove(atOffsets: indexSet)
}
private func addNewWord() {
let answer = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
self.userSettings.favList.append(answer)
guard answer.count > 0 else {
return
}
newWord = ""
}
}
A better approach to follow the SwiftUI idiom is to use the .environmentObject() modifier.
When you declare your app:
struct AppScene: App {
#StateObject private var userSettings = UserSettings() // Use state object to persist the object
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(userSettings) // Inject userSettings into the environment
}
}
}
and then in you ContentView you can reach into your environment and get the object:
struct ContentView: View {
#EnvironmentObject private var userSettings: UserSettings
var body: some View {
Text("Number of items in favList: \(userSettings.favList.count)")
}
}
You need to use same instance of UserSettings in all views where you want to have observed user settings, like
class UserSettings: ObservableObject {
static let global = UserSettings()
//... other code
}
and now
struct FavButton: View {
#ObservedObject var userSettings = UserSettings.global // << here !!
// ... other code
}
and
struct FavView: View {
#ObservedObject var userSettings = UserSettings.global // << here !!
// ... other code
}
I made a list with a navigation view and I'm passing the ID to a detailed view but I need to fetch more data from a different endpoint to get more data in the detail view. Unfortunately, I got stuck to update the ObservableObject to load the detailed data for one post.
Where and how should I set the postID in order to fetch detailed data ?
// PostsListView
struct PostsListView: View {
#ObservedObject var obs = postsObserver()
var body: some View {
NavigationView{
ScrollView(){
VStack{
ForEach(obs.posts, id: \.name) {post in
NavigationLink(destination: PostView(postId: post.id)){
VStack(alignment: .leading){
Text(post.name)
}
}
.buttonStyle(PlainButtonStyle())
}
}.padding(.horizontal)
}.id(UUID().uuidString)
}
}
}
class postsObserver: ObservableObject{
#Published var posts = [Post]()
init() {
AF.request("http://localhost/posts").responseData{(data) in
let json = try! JSON(data: data.data!)
for i in json {
self.posts.append(
Post(
id: i.1["id"].intValue,
name: i.1["name"].stringValue,
)
)
}
}
}
}
// PostView.swift
struct PostView: View {
#State var postId: Int
#ObservedObject var obs = postObserver()
var body: some View {
VStack {
Text("\(obs.specificValue)")
Text("\(postId)")
}
}
}
class postObserver: ObservableObject{
// #Published var post = Post
// AF.request("http://localhost/posts/\(postId)/details").responseData{(data) in
}
You can do it in init, like below
struct PostView: View {
private let postId: Int // << not state needed
#ObservedObject var obs: postObserver // << declare
init(postId: Int) {
self.postId = postId
self.obs = postObserver(postId: postId) // << initialize
}
var body: some View {
VStack {
if obs.specificValue != nil { // wait till fetch
Text("\(obs.specificValue!)")
}
Text("\(postId)")
}
}
}
class postObserver: ObservableObject{
#Published var specificValue: ValueType?
init(postId: Int} {
AF.request("http://localhost/posts/\(postId)/details").responseData{(data) in
... decoding code here
DispatchQueue.main.async {
self.specificValue = ...
}
}
}
In swift UI I want the content view to be my root view for my app with a conditional setup to check if the users is logged in or not. If the user is logged in a list view shows other wise show the login view so the user can log in. Based on my research i could not find a best way to do this.
In my case I can not get the solution I found to work and do not know if it is the best solution.
import SwiftUI
struct ContentView: View {
#ObservedObject var userAuth: UserAuth = UserAuth()
// MARK: - View
#ViewBuilder
var body: some View {
if !userAuth.isLoggedin {
return LoginView().environmentObject(userAuth)
}
return BookList()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import Combine
class UserAuth: ObservableObject {
let didChange = PassthroughSubject<UserAuth,Never>()
// required to conform to protocol 'ObservableObject'
let willChange = PassthroughSubject<UserAuth,Never>()
func login() {
// login request... on success:
self.isLoggedin = true
}
func logout() {
// login request... on success:
self.isLoggedin = false
}
var isLoggedin = false {
didSet {
didChange.send(self)
}
// willSet {
// willChange.send(self)
// }
}
}
When running this all i get is a white screen. It seems that the view builder might be the problem but removing that i get a opaque error on content view
There two problems with provided code snapshot: 1) incorrect view builder content, and 2) incorrect model.
See below both fixed. Tested with Xcode 11.4 / iOS 13.4
struct ContentView: View {
#ObservedObject var userAuth: UserAuth = UserAuth()
// MARK: - View
#ViewBuilder // no need return inside
var body: some View {
if !userAuth.isLoggedin {
LoginView().environmentObject(userAuth)
}
else {
BookList()
}
}
}
import Combine
class UserAuth: ObservableObject {
#Published var isLoggedin = false // published property to update view
func login() {
// login request... on success:
self.isLoggedin = true
}
func logout() {
// login request... on success:
self.isLoggedin = false
}
}
how about just this:
var body: some View {
if !userAuth.isLoggedin {
LoginView().environmentObject(userAuth)
} else {
BookList()
}
}