Changing the property of observableObject from another observableObject - ios

There is a problem of the following nature: it is necessary to create an authorization window for the application, the most logical solution I found the following implementation (I had to do this because the mainView has a tabView which behaves incorrectly if it is in a navigationView)
struct ContentView: View {
#EnvironmentObject var vm: AppSettings
var body: some View {
if vm.isLogin {
MainView()
} else {
LoginView()
}
}
AppSettings looks like this:
struct MyApp: App {
#StateObject var appSetting = AppSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appSetting)
}
}
}
class AppSettings: ObservableObject {
#Published var isLogin = false
}
By default, the user will be presented with an authorization window that looks like this:
struct LoginView: View {
#StateObject var vm = LoginViewModel()
var body: some View {
NavigationView {
VStack {
TextField("Email", text: $vm.login)
TextField("Password", text: $vm.password)
Button {
vm.auth()
} label: {
Text("SignIn")
}
}
}
}
}
And finally the loginViewModel looks like this:
class LoginViewModel: ObservableObject {
#Published var login = ""
#Published var password = ""
//#Published var appSettings = AppSettings() -- error on the first screenshot
//or
//#EnvironmentObject var appSettings: AppSettings -- error on the second screenshot
func auth() {
UserAPI().Auth(req: LoginRequest(email: login, password: password)) { response, error in
if let err = error {
// Error Processing
} else if let response = response {
Defaults.accessToken = response.tokens.accessToken
Defaults.refreshToken = response.tokens.refreshToken
self.appSettings.isLogin = true
}
}
}
}
1 error - Accessing StateObject's object without being installed on a View. This will create a new instance each time
2 error - No ObservableObject of type AppSettings found. A View.environmentObject(_:) for AppSettings may be missing as an ancestor of this view
I ask for help, I just can not find a way for the interaction of two observableObject. I had to insert all the logic into the action of the button to implement such functionality
In addition to this functionality, it is planned to implement an exit from the account by changing the isLogin variable to false in various cases or use other environment variables to easily implement other functions
The example is deliberately simplified for an easy explanation of the situation

I would think using only LoginViewModel at top level would be the easiest way to solve this. But if you want to keep both, you can synchronise them with an .onChanged modifier.
class LoginViewModel: ObservableObject {
#Published var login = ""
#Published var password = ""
#Published var isLogin = false
func auth() {
UserAPI().Auth(req: LoginRequest(email: login, password: password)) { response, error in
if let err = error {
// Error Processing
} else if let response = response {
Defaults.accessToken = response.tokens.accessToken
Defaults.refreshToken = response.tokens.refreshToken
isLogin = true
}
}
}
}
struct LoginView: View {
#StateObject var vm = LoginViewModel()
// grab the AppSettings from the environment
#EnvironmentObject var appSetting: AppSettings
var body: some View {
NavigationView {
VStack {
TextField("Email", text: $vm.login)
TextField("Password", text: $vm.password)
Button {
vm.auth()
} label: {
Text("SignIn")
}
// synchronise the viewmodels here
.onChange(of: vm.isLogin) { newValue in
appSetting.isLogin = newValue
}
}
}
}
}

Related

SwiftUI - "Argument passed to call that takes no arguments"?

I have an issue with the coding for my app, where I want to be able to scan a QR and bring it to the next page through navigation link. Right now I am able to scan a QR code and get a link but that is not a necessary function for me. Below I attached my code and got the issue "Argument passed to call that takes no arguments", any advice or help would be appreciated :)
struct QRCodeScannerExampleView: View {
#State private var isPresentingScanner = false
#State private var scannedCode: String?
var body: some View {
VStack(spacing: 10) {
if let code = scannedCode {
//error below
NavigationLink("Next page", destination: PageThree(scannedCode: code), isActive: .constant(true)).hidden()
}
Button("Scan Code") {
isPresentingScanner = true
}
Text("Scan a QR code to begin")
}
.sheet(isPresented: $isPresentingScanner) {
CodeScannerView(codeTypes: [.qr]) { response in
if case let .success(result) = response {
scannedCode = result.string
isPresentingScanner = false
}
}
}
}
}
Page Three Code
import SwiftUI
struct PageThree: View {
var body: some View {
Text("Hello, World!")
}
}
struct PageThree_Previews: PreviewProvider {
static var previews: some View {
PageThree()
}
}
You forgot property:
struct PageThree: View {
var scannedCode: String = "" // << here !!
var body: some View {
Text("Code: " + scannedCode)
}
}
You create your PageThree View in two ways, One with scannedCode as a parameter, one with no params.
PageThree(scannedCode: code)
PageThree()
Meanwhile, you defined your view with no initialize parameters
struct PageThree: View {
var body: some View {
Text("Hello, World!")
}
}
For your current definition, you only can use PageThree() to create your view. If you want to pass value while initializing, change your view implementation and consistently using one kind of initializing method.
struct PageThree: View {
var scannedCode: String
var body: some View {
Text(scannedCode)
}
}
or
struct PageThree: View {
private var scannedCode: String
init(code: String) {
scannedCode = code
}
var body: some View {
Text(scannedCode)
}
}
This is basic OOP, consider to learn it well before jump-in to development.
https://docs.swift.org/swift-book/LanguageGuide/Initialization.html

Binding not working as expected when button is pressed

I have a button that's supplied with data and this is used to unfollow or follow a user. What should happen is that I press the button, it changes the isFollowing property on the user, and then the text updates from unfollow to follow. However, this doesn't work. Here's my code that can be put into a playground (simplified for the purposes of this just to show the core elements):
struct User: Hashable, Identifiable {
let id = UUID()
var isFollowing: Bool
}
final class MyModel: ObservableObject {
#Published var users: [User] = [User(isFollowing: true)]
func unfollow(_ user: Binding<User>) async throws {
user.wrappedValue.isFollowing = false
}
}
struct ContainerView: View {
#EnvironmentObject private var model: MyModel
var body: some View {
UserListView(users: $model.users)
}
}
final class PagedUsers: ObservableObject {
#Published var loadedUsers: [Binding<User>] = []
#Binding var totalUsers: [User]
init(totalUsers: Binding<[User]>) {
self._totalUsers = totalUsers
let firstUser = $totalUsers.first!
loadedUsers.append(firstUser)
}
}
struct UserListView: View {
#StateObject private var pagedUsers: PagedUsers
init(users: Binding<[User]>) {
self._pagedUsers = StateObject(wrappedValue: PagedUsers(totalUsers: users))
}
var body: some View {
ForEach(pagedUsers.loadedUsers) { user in
MyView(user: user)
}
}
}
struct MyView: View {
#EnvironmentObject private var model: MyModel
#Binding var user: User
var body: some View {
Button(
action: {
Task {
do {
try await model.unfollow($user)
} catch {
print("Error!", error)
}
}
},
label: {
Text(user.isFollowing ? "Unfollow" : "Follow")
}
)
}
}
PlaygroundPage.current.setLiveView(
ContainerView()
.environmentObject(MyModel())
)
I think it's not working because of something to do with the passing of bindings, but I can't quite work out why. Possibly it's the setup of PagedUsers? However, this needs to be there because in my app code I essentially pass all the user data to it, and return "pages" of users from this, which gets added to as the user scrolls.
I don't fully understand why you need two classes for users ... why not put them in one in different #Published vars?
IMHO then you don't need any bindings at all!
Here is a working code with only one class, hopefully you can build from this:
import SwiftUI
#main
struct AppMain: App {
#StateObject private var model = MyModel()
var body: some Scene {
WindowGroup {
ContainerView()
.environmentObject(model)
}
}
}
struct User: Hashable, Identifiable {
let id = UUID()
var isFollowing: Bool
}
class MyModel: ObservableObject {
#Published var users: [User] = [User(isFollowing: true), User(isFollowing: true), User(isFollowing: true)]
func unfollow(_ user: User) async throws {
if let index = users.firstIndex(where: {$0.id == user.id}) {
self.objectWillChange.send()
users[index].isFollowing = false
}
}
}
struct ContainerView: View {
#EnvironmentObject private var model: MyModel
var body: some View {
UserListView(users: model.users)
}
}
struct UserListView: View {
let users: [User]
var body: some View {
List {
ForEach(users) { user in
MyView(user: user)
}
}
}
}
struct MyView: View {
#EnvironmentObject private var model: MyModel
var user: User
var body: some View {
Button(
action: {
Task {
do {
try await model.unfollow(user)
} catch {
print("Error!", error)
}
}
},
label: {
Text(user.isFollowing ? "Unfollow" : "Follow")
}
)
}
}

Work with Binding inside an external function in SwiftUI

I'm currently learning SwiftUI and want to develop my own app. I have designed a LoginView and a LoginHandler that should take care of all the logic behind a login. When the user enters the wrong username/password, an Alert should appear on the screen. I solved this with the state variable loginError. But now comes the tricky part, as i want to pass a binding of this variable to my login function in the LoginHandler. Take a look at the following code:
import SwiftUI
struct LoginView: View
{
#EnvironmentObject var loginHandler: LoginHandler
#State private var username: String = ""
#State private var password: String = ""
#State private var loginError: Bool = false
...
private func login()
{
loginHandler.login(username: username, password: password, error: $loginError)
}
}
I am now trying to change the value of error inside my login function:
import Foundation
import SwiftUI
class LoginHandler: ObservableObject
{
public func login(username: String, password: String, error: Binding<Bool>)
{
error = true
}
}
But I'm getting the error
Cannot assign to value: 'error' is a 'let' constant
which makes sense I think because you can't edit the parameters in swift. I have also tried _error = true because I once saw the underscore in combination with a binding, but this doesn't worked either.
But then I came up with a working solution: error.wrappedValue = true. My only problem with that is the following statement from Apples Developer Documentation:
This property provides primary access to the value’s data. However, you don’t access wrappedValue directly. Instead, you use the property variable created with the #Binding attribute.
Although I'm super happy that it works, I wonder if there is any better way to solve this situation?
Update 20.3.21: New edge case
In the comment section I mentioned a case where you don't know how many times your function will be used. I will now provide a little code example:
Imagine a list of downloadable files (DownloadView) that you will get from your backend:
import SwiftUI
struct DownloadView: View
{
#EnvironmentObject var downloadHandler: DownloadHandler
var body: some View
{
VStack
{
ForEach(downloadHandler.getAllDownloadableFiles())
{
file in DownloadItemView(file: file)
}
}
}
}
Every downloadable file has a name, a small description and its own download button:
import SwiftUI
struct DownloadItemView: View
{
#EnvironmentObject var downloadHandler: DownloadHandler
#State private var downloadProgress: Double = -1
var file: File
var body: some View
{
HStack
{
VStack
{
Text(file.name)
Text(file.description)
}
Spacer()
if downloadProgress < 0
{
// User can start Download
Button(action: {
downloadFile()
})
{
Text("Download")
}
}
else
{
// User sees download progress
ProgressView(value: $downloadProgress)
}
}
}
func downloadFile()
{
downloadHandler.downloadFile(file: file, progress: $downloadProgress)
}
}
And now finally the 'DownloadHandler':
import Foundation
import SwiftUI
class DownloadHandler: ObservableObject
{
public func downloadFile(file: File, progress: Binding<Double>)
{
// Example for changing the value
progress = 0.5
}
}
You can update parameters of a function as well, here is an example, this not using Binding or State, it is inout!
I am now trying to change the value of error inside my login function:
Cannot assign to value: 'error' is a 'let' constant
So with this method or example you can!
struct ContentView: View {
#State private var value: String = "Hello World!"
var body: some View {
Text(value)
.padding()
Button("update") {
testFuction(value: &value)
}
}
}
func testFuction(value: inout String) {
value += " updated!"
}
I see what you're trying to do, but it will cause problems later on down the line because you're dealing with State here. Now one solution would be:
You could just abstract the error to the class, but then you would have the username and password in one spot and the error in another.
The ideal solution then is to abstract it all away in the same spot. Take away all of the properties from your view and have it like this:
import SwiftUI
struct LoginView: View
{
#EnvironmentObject var loginHandler: LoginHandler
// login() <-- Call this when needed
...
}
Then in your class:
import Foundation
import SwiftUI
#Published error: Bool = false
var username = ""
var password = ""
class LoginHandler: ObservableObject
{
public func login() {
//If you can't login then throw your error here
self.error = true
}
}
The only left for you to do is to update the username and password` and you can do that with this for example
TextField("username", text: $loginHandler.username)
TextField("username", text: $loginHandler.password)
Edit: Adding an update for the edge case:
import SwiftUI
struct ContentView: View {
var body: some View {
LazyVGrid(columns: gridModel) {
ForEach(0..<20) { x in
CustomView(id: x)
}
}
}
let gridModel = [GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 10),
GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 10),
GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 10)
]
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct CustomView: View {
#State private var downloaded = false
#State private var progress = 0
#ObservedObject private var viewModel = StateManager()
let id: Int
var body: some View {
showAppropriateView()
}
#ViewBuilder private func showAppropriateView() -> some View {
if viewModel.downloadStates[id] == true {
VStack {
Circle()
.fill(Color.blue)
.frame(width: 50, height: 50)
}
} else {
Button("Download") {
downloaded = true
viewModel.saveState(of: id, downloaded)
}
}
}
}
final class StateManager: ObservableObject {
#Published var downloadStates: [Int : Bool] = [:] {
didSet { print(downloadStates)}
}
func saveState(of id: Int,_ downloaded: Bool) {
downloadStates[id] = downloaded
}
}
I didn't add the progress to it because I'm short on time but I think this conveys the idea. You can always abstract away the individual identity needed by other views.
Either way, let me know if this was helpful.

#Published property in #EnvironmentObject doesn't update the root view of my application

I am trying to make a router object for my application using #EnvironmentObject. But the problem is the #Published property doesn't update the root view when the root view type is updated.
How it should work
A user clicks Sign in with Apple button
Update the router.currentPage property of #EnvironmentObject when a user logs in successfully.
RootView get notified for updating router.currentPage and change the root view in accordance to the updated currentPage type.
Here are my codes below.
MainApp.swift
var body: some Scene {
WindowGroup {
RootView().environmentObject(ViewRouter())
}
}
ViewRouter.swift
Code Block
enum Page {
case signin
case tasklist
}
final class ViewRouter: ObservableObject {
#Published var currentPage: Page = .signin
}
RootView.swift
struct RootView: View {
#EnvironmentObject var router: ViewRouter
var body: some View {
if router.currentPage == .signin {
SigninView()
} else {
TaskListView()
}
}
}
SigninView.swift
struct SigninView: View {
#EnvironmentObject var router: ViewRouter
#State var signInHandler: SignInWithAppleCoordinator?
var window: UIWindow? {
guard let scene = UIApplication.shared.connectedScenes.first,
let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate,
let window = windowSceneDelegate.window else {
return nil
}
return window
}
var body: some View {
MyAppleIDButton().colorScheme(.light)
.frame(width: 280, height: 38, alignment: .center)
.onTapGesture {
signInWithAppleButtonTapped()
}
}
func signInWithAppleButtonTapped() {
guard let _window = self.window else { return }
signInHandler = SignInWithAppleCoordinator(window: _window)
signInHandler?.signIn { (user) in
router.currentPage = .tasklist
}
}
}
Update
I think I found an answer to this issue.
I created a state isLoggedIn which is checking whether or not Sign in with Apple is done successfully.
#State var isLoggedIn: Bool = false
Then I added View Modifier onChange which is checking the value change of isLoggedIn above. Inside the onChange I assigned a new value to router.currentPage like below.
.onChange(of: isLoggedIn, perform: { isLoggedIn in
if isLoggedIn {
router.currentPage = .tasklist
} else {
router.currentPage = .signin
}
})
But I am still not sure of why it doesn't work in the closure of SigninWithApple button.

SwiftUI Open view in two different ways using view model pattern

I need to open a view called RequestDetails.
There are two cases in which this view can be opened.
Providing the data to open this request
Providing a reference to a Firestore document
In this case I do not have all the details of this specific Request but I have just a reference to the Firestore document. I am using this reference to make a query as soon as this view appears and get the details about this Request RequestDetail(reference: reference)
class RequestViewModel: ObservableObject {
#Published var request: RequestModel?
#Published var requestReference: DocumentReference?
init(request: RequestModel? = nil, requestReference: DocumentReference? = nil) {
self.request = request
self.requestReference = requestReference
}
func loadRequest() {
FirestoreService().fetchDocument(documentReference: self.requestReference) { (request: RequestModel) in
DispatchQueue.main.async {
self.request = request
}
}
}
}
struct RequestDetails: View {
#State var reference: DocumentReference?
#State var request: RequestModel?
#ObservedObject var vm: RequestViewModel
var body: some View {
VStack {
if request != nil {
Text(self.request?.senderFirstName)
}
}.onAppear {
if self.vm.package == nil {
self.vm.loadRequest()
}
}
}
}
struct Home: View {
var request: RequestModel
var reference: DocumentReference
var body: some View {
VStack {
RequestDetail(request: request)
RequestDetail(reference: reference)
}
}
}
The thing is that I'm getting a lot of errors and I'm wondering if the logic behind is ok or not. Am I using the view model pattern in the right way?
The following would be appropriate to follow MVVM concept
struct RequestDetails: View {
#ObservedObject var vm: RequestViewModel
var body: some View {
VStack {
if self.vm.request != nil {
Text(self.vm.request!.senderFirstName)
}
}.onAppear {
if self.vm.request == nil {
self.vm.loadRequest()
}
}
}
}
struct Home: View {
var request: RequestModel
var reference: DocumentReference
var body: some View {
VStack {
RequestDetail(vm: RequestViewModel(request: request))
RequestDetail(vm: RequestViewModel(reference: reference))
}
}
}

Resources