2

I'm new to swift/swiftui programming.

I have two environment variables that user puts in through TextFields. Based on these two text fields, I have to initialise my client. I used UserDefaults instead of the EnvironmentObject because these two fields can be considered user settings.

Here is the UserPreferences Class

class UserPreferences: ObservableObject {
    @Published var accessKey: String {
        didSet {
            UserDefaults.standard.set(accessKey, forKey: "accessKey")
        }
    }
    
    @Published var secretKey: String {
        didSet {
            UserDefaults.standard.set(secretKey, forKey: "secretKey")
        }
    }
    
    @Published var region: String {
        didSet {
            UserDefaults.standard.set(region, forKey: "region")
        }
    }
    
    var regions = ["us-east-1", "us-east-2"]
            
    init() {
        self.accessKey = UserDefaults.standard.object(forKey: "accessKey") as? String ?? ""
        self.secretKey = UserDefaults.standard.object(forKey: "secretKey") as? String ?? ""
        self.region = UserDefaults.standard.object(forKey: "region") as? String ?? "us-east-1"
    }
    
}

Based on the accessKey and secretKey of the user, I need to create a client. I tried having this client as additional var inside the user preferences class.

var client: AWSClient {
        return AWSClient(
            credentialProvider: .static(accessKeyId: accessKey, secretAccessKey: secretKey),
            httpClientProvider: .createNew)
    }

But this isn't really a user setting. On top of that, I'm also getting the error that I need to shutdown the AWSClient before the deinit. I was able to make it work with hardcoded accessKey and secretKey and shutdown the client in the deinit of the class like below.

    deinit {
        do {
            try client.syncShutdown()
        } catch {
            print("client shutdown error deinit")
        }
    }

Another way I tried is not having the client in the UserPreferences Object and instead create a new client variable in my view itself. But in a view, I have a helper function and I'm not able to do a graceful shutdown of the client as functions and structs dont have deinit in them.

But I feel like there must be a better way of initialising this client and use it in any of my views.

Thank you for your help.

Edit:

This is my preferences View.

struct PreferencesView: View {
    
    @ObservedObject var userPreferences = UserPreferences()
    
    var body: some View{
        VStack{
            
            Text("Preferences")
                .font(.title)
                
            HStack{
                Text("Access Key:")
                SecureField("Access Key", text: $userPreferences.accessKey)
            }
            HStack{
                Text("Access Secret:")
                SecureField("Access Secret", text: $userPreferences.secretKey)
            }
            HStack {
                Picker(selection: $userPreferences.region, label: Text("Region:")) {
                    ForEach(userPreferences.regions, id: \.self) { region in Text(region)
                    }
                }
                Spacer()
                Button{checkAWSClient(accessKey: userPreferences.accessKey, secretKey: userPreferences.secretKey)} label: {
                    Text("Check AWS Credentials")
                }
                Button{print("done button clicked")} label:{
                    Text("Done")
                }
            }
            Spacer()
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
        .padding()
    }
}

It has two text fields for the two user preference strings, that I'm storing as user defaults. And there is a button when clicked will check the connection to some external services using the user preferences.

And the function CheckAWSClient is as follows

func checkAWSClient(accessKey: String, secretKey: String){
    print("Checking aws client")
    let client = AWSClient(credentialProvider: .static(accessKeyId: accessKey, secretAccessKey: secretKey), httpClientProvider: .createNew)
    print(client)
    let s3 = S3(client: client, region: .useast1)
    s3.listBuckets()
        .whenComplete {response in
            switch response {
            case .failure(let error):
                print(error)
                print("Failure s3")
            
            case .success(let output):
                print(output)
                print("Success s3")
            
            }
        }
    
    let ec2 = EC2(client: client, region: .useast1)
    let describeInstancesRequest = EC2.DescribeInstancesRequest(dryRun: false)
    ec2.describeInstances(describeInstancesRequest)
        .whenComplete {response in
            switch response {
            case .failure(let error):
                print(error)
                print("Failure EC2")
            case .success(let output):
                print(output)
                print("Success EC2")
            }
        }    
}

But I'm getting an error saying I need to do client.shutdown() in deinit. But what does it mean. I'm initialising the client in a helper function in some view.

Here is the error

Assertion failed: AWSClient not shut down before the deinit. Please call client.syncShutdown() when no longer needed.: file SotoCore/AWSClient.swift, line 95
2021-02-23 03:41:20.839170+0530 EC2 Menu Bar[3047:23906299] Assertion failed: AWSClient not shut down before the deinit. Please call client.syncShutdown() when no longer needed.: file SotoCore/AWSClient.swift, line 95

tl;dr I need to make use of user default settings that he sets and make a db connection sort of thing. So that I can do various rest queries from different views. What is the best of doing that?

syllogismos
  • 600
  • 2
  • 15
  • 39
  • It's hard to tell what you're trying to do without more code. But for starters, UserDefaults and EnvironmentObject are not interchangeable, they are very different things. You can store values in UserDefaults to persist between sessions. That is separate from ObservedObject/StateObject/EnvironmentObject which will manage the life of a Class within your app. You could probably initialize your UserPreferences class as an @ EnvironmentObject within your @ main app file and then use it for AWS. – nicksarno Feb 22 '21 at 21:35
  • Yes, i want these strings to persist between sessions. But also for the user to set them. Once they are set. I want to use those strings to make an aws client using the soto swift package. And query ec2 instances and etc and show them in a view. I'm not able to figure out how to use these strings user entered and make an aws client. – syllogismos Feb 22 '21 at 22:07
  • 1
    You can get rid of most of your userPreferences class and replace it with 3 lines of \@AppStorage. E.g. @AppStorage("accessKey") var accessKey = "". Depending on where @AppStorage runs in the app lifecycle, in addition to reducing the amount of code needed, it might make initializing your client cleaner. – Nicholas Rees Mar 01 '21 at 21:40

1 Answers1

1

Assuming all those calls are asynchronous I suppose the function should look like the following. The main idea is to capture client till the end of all activity (because it is created on stack so has no owner and will be destroyed as soon as no more refereces to it), and shutdown it only on failure or at the very end.

func checkAWSClient(accessKey: String, secretKey: String){
    print("Checking aws client")
    let client = AWSClient(credentialProvider: .static(accessKeyId: accessKey, secretAccessKey: secretKey), httpClientProvider: .createNew)
    print(client)
    
    let shutdown = {[client] in     // << capture client
        do {
            try client.syncShutdown()
        } catch {
            print("client shutdown error deinit")
        }
    }
    
    let s3 = S3(client: client, region: .useast1)
    s3.listBuckets()
        .whenComplete {response in
            switch response {
            case .failure(let error):
                print(error)
                print("Failure s3")
                
                // failure: client is not needed anymore - shutdown
                shutdown()
                
            case .success(let output):
                print(output)
                print("Success s3")
                
                // continue with EC2 only on list success
                let ec2 = EC2(client: client, region: .useast1)
                let describeInstancesRequest = EC2.DescribeInstancesRequest(dryRun: false)
                ec2.describeInstances(describeInstancesRequest)
                    .whenComplete {response in
                        switch response {
                        case .failure(let error):
                            print(error)
                            print("Failure EC2")
                        case .success(let output):
                            print(output)
                            print("Success EC2")
                        }
                        
                        //completed: client is not needed anymore - shutdown
                        shutdown()
                    }
            }
        }
}

Note: cannot test, so some typo fixing or tuning might be needed

Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Thank you for helping me. it is working. I am able to shutdown the client properly and the ui is not freezing anymore. But still I need to figure out a better way to initialize the client and use the same client for all the views and close it in a single place instead of having this nested structure. – syllogismos Mar 02 '21 at 01:49