How do I create a http connection using environment variables that are stored as UserDefaults in swiftui?

Viewed 145

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?

1 Answers

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

Related