Initializing an s3 client in AWS SDK for Swift - ios

The docs for the AWS SDk for Swift describe how to instantiate a service client:
let s3Client = try S3Client(region: "us-east-1")
However since this statement can throw errors it needs to be wrapped in a do catch block as shown on a separate documentation page:
do {
let s3 = try S3Client(region: "af-south-1")
// .....
} catch {
dump(error, name: "Error accessing S3 service")
exit(1)
}
I've created a public S3Manager class that will contain the specific functions I want to use in my code for manipulating s3 buckets. Typically I would instantiate the client in the class initializer but since it must be wrapped in a do catch block I have taken a different approach:
public class S3manager : ObservableObject {
public var client: S3Client?
public func initializeClient() {
do {
client = try S3Client(region: "us-west-2")
} catch {
fatalError("Error creating S3 client: \(error)")
}
}
public func listBucketFiles(bucket: String) async throws -> [String] {
if let clientInstance = client {
// ...
}
}
}
In my views I will reference the class as a StateObject or ObservedObject as appropriate and be certain to call initializeClient before calling any other methods I implement in the class (in the initial view's onAppear() method for example) Is this the best approach though?

I can't confirm that the code below works exactly as expected because the SDK is not finding my credentials in ~/.aws/credentialsor as environment variables, but I changed my approach to contain all of thes3` logic in it's own respective class:
import Foundation
import AWSS3
import ClientRuntime
import AWSClientRuntime
public class S3manager : ObservableObject {
private var client: S3Client?
public var files: [String]?
init() {
Task(priority: .high) {
do {
client = try S3Client(region: "us-west-2")
files = try await listBucketFiles(bucket: "bucket_name")
} catch {
print(error)
}
}
}
// from https://docs.aws.amazon.com/sdk-for-swift/latest/developer-guide/examples-s3-objects.html
public func listBucketFiles(bucket: String) async throws -> [String] {
if let clientInstance = client {
let input = ListObjectsV2Input(
bucket: bucket
)
let output = try await clientInstance.listObjectsV2(input: input)
var names: [String] = []
guard let objList = output.contents else {
return []
}
for obj in objList {
if let objName = obj.key {
names.append(objName)
}
}
return names
} else {
print("Client has not been initialized!")
return [String]()
}
}
}
This has the advantage that in ContentView I can simply reference the class member as follows:
struct ContentView: View {
#StateObject private var s3 = S3manager()
var body: some View {
VStack {
List(s3.files ?? ["Content Loading..."], id: \.self) {
Text($0)
}
}
}
}

Related

Escaping closure captures mutating 'self' parameter, Firebase

I have the following code, How can i accomplish this without changing struct into class. Escaping closure captures mutating 'self' parameter,
struct RegisterView:View {
var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
self.names = name //here is the error
}){(error) in
print("Error: \(error)")
}
init(){
LoadPerson()
}a
var body:some View{
//ui code
}
}
Firebasemanager.swift
struct FirebaseManager {
func fetchPerson(
success: #escaping (Person) -> (),
failure: #escaping (String) -> ()
) {
Database.database().reference().child("Person")
.observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
success(Person(dictionary: dictionary))
}
}) { (error) in
failure(error.localizedDescription)
}
}
}
SwiftUI view can be created (recreated) / copied many times during rendering cycle, so View.init is not appropriate place to load some external data. Use instead dedicated view model class and load explicitly only when needed.
Like
class RegisterViewModel: ObservableObject {
#Published var names = [String]()
func loadPerson() {
// probably it also worth checking if person has already loaded
// guard names.isEmpty else { return }
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
struct RegisterView: View {
// in SwiftUI 1.0 it is better to inject view model from outside
// to avoid possible recreation of vm just on parent view refresh
#ObservedObject var vm: RegisterViewModel
// #StateObject var vm = RegisterViewModel() // << only SwiftUI 2.0
var body:some View{
Some_Sub_View()
.onAppear {
self.vm.loadPerson()
}
}
}
Make the names property #State variable.
struct RegisterView: View {
#State var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success: { person in
guard let name = person.name else { return }
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
//...
}

Struct cross module initializers

I have been working on a project which has some different modules.
In First Module:
I have somthing like this
public struct User: Codable {
// MARK: Stored Properties
public let id: String
public let name: String
public let email: String
}
extension User {
public enum FetchProfile { }
}
extension User.FetchProfile {
public struct Input {
public let userId: String
}
public struct Output {
public var user: User
}
}
And in the other module:
import FirstModule
extension User.FetchProfile.Output {
init(object: JSON) throws {
let decoder = JSONDecoder()
let jsonData = Data(value.description.utf8)
self.user = try! decoder.decode(User.self, from: jsonData)
}
}
if i use swift 5 i got this error:
'self' used before 'self.init' call or assignment to 'self'
'self.init' isn't called on all paths before returning from
initializer
but in swift 4 i have this warning:
Initializer for struct 'User.FetchProfile.Output' must use
"self.init(...)" or "self = ..." because it is not in module
'SecondModule'
I already read what in this link:
https://github.com/apple/swift-evolution/blob/master/proposals/0189-restrict-cross-module-struct-initializers.md
But I'm still did not get it, what should i do to fix this issue.
Try the following
extension User.FetchProfile {
public struct Input {
public let userId: String
}
public struct Output {
public var user: User? = nil // << here !!
}
}
and in second module
extension User.FetchProfile.Output {
init(object: JSON) throws {
self.init() // << here !!
let decoder = JSONDecoder()
let jsonData = Data(value.description.utf8)
self.user = try? decoder.decode(User.self, from: jsonData)
}
}

Swift issues getting images from URL in forEach list

I'm currently trying to transition from a web developer only to a mobile and web developer and have been having great difficulties learning swift, I think I learn something and write it only to have numerous errors. Right now I have the following code to get an image from my server inside of a foreach loop.
struct MainNotLoggedInView: View {
#ObservedObject var fetcher = Fetcher()
func getPic(urlLink: String) -> Image? {
let baseURL = "https://mywebsite.com"
let url = URL(string: baseURL + urlLink)
let data = try? Data(contentsOf: url!)
let image = UIImage(data: data!)
let image2 = Image(uiImage: (image ?? nil)!)
return image2
}
var body: some View{
VStack{
TabView {
ScrollView {
ZStack {
if fetcher.hasFinished == true {
VStack {
Text("Featured").font(.largeTitle).padding()
ForEach(fetcher.ac?.ac.featuredAc ?? []) { result in
VStack {
Text(result.name)
Image(self.getPic(urlLink: result.acPic))
}
}
}
} else {
Text("Featured").font(.largeTitle).padding()
}
}
}.tabItem {
Text("Featured").font(.subheadline)
}
OtherView().tabItem {
Text("Other").font(.subheadline)
}
}
}
}
}
I assume in order to figure out what is going on, I should also include the JSON fetcher script and the structures, which are below
import SwiftUI
import Combine
import Foundation
public struct AcModel: Codable, Identifiable {
public let id: Int
public let name: String
public let acPic: String
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case acPic = "picture_link"
}
}
public struct AcModel2: Codable {
public let location: String
private var popular: [String:AcModel]
public var popularAc: [AcModel] {
get {
return Array(self.popular.values)
}
}
private var featured: [String:AcModel]
public var featuredAc: [AcModel] {
get {
return Array(self.featured.values)
}
}
}
public struct AcModel: Codable {
public let ac: AcModel2
}
public class Fetcher: ObservableObject {
public let objectWillChange = PassthroughSubject<Fetcher,Never>()
#Published var hasFinished: Bool = false {
didSet {
objectWillChange.send(self)
}
}
var ac: AcModel?
init(){
guard let url = URL(string: "https://mywebsite.com/api/loadactivitiesguest") else { return }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode(AcModel.self, from: d)
DispatchQueue.main.async {
self.ac = decodedLists
self.hasFinished = true
print("dispatching")
}
} else {
print("No Data")
}
} catch {
print("Error")
}
}.resume()
}
}
I'm sure because I'm a web developer by trade I'm thinking about it the wrong way, but as you can tell from the code, I'm trying to get the picture inside the foreach using the swift method of retrieving pictures from a server, and trying to display it. The code as I have it written has the error "Type of expression is ambiguous without more context". This error is on the line inside the actual view where I try to call the function; "Image(self.getPic(urlLink: result.acPic))"
I bolded the actual words highlight by the error.
Of course, the acPic variable is an actual variable, which is not optional, of the structure. I've seen a lot of other StackOverflow posts about this error, but as with several of the swift errors, it seems the same error message can be caused by several difference types of code with different purposes, and in combination with my lack of swift experience, I have difficulty understanding the relation between what they did wrong and what I did wrong.
From reading around however, I read that I should use something called Kingfisher to cache the images as there is hundreds. How do I install those git repositories into my project? Just a side question, no need to really answer there's probably hundreds of videos.
Any help would be appreciated.
Update
Image(self.getPic(urlLink: result.acPic))
to
self.getPic(urlLink: result.acPic)
Image hasn't a init method which gets an optional Image object and returns an Image object

How to bind data from viewModel in view with rxSwift and Moya?

I'm trying to create an app to get some news from an API and i'm using Moya, RxSwift and MVVM.
This is my ViewModel:
import Foundation
import RxSwift
import RxCocoa
public enum NewsListError {
case internetError(String)
case serverMessage(String)
}
enum ViewModelState {
case success
case failure
}
protocol NewsListViewModelInput {
func viewDidLoad()
func didLoadNextPage()
}
protocol MoviesListViewModelOutput {
var newsList: PublishSubject<NewsList> { get }
var error: PublishSubject<String> { get }
var loading: PublishSubject<Bool> { get }
var isEmpty: PublishSubject<Bool> { get }
}
protocol NewsListViewModel: NewsListViewModelInput, MoviesListViewModelOutput {}
class DefaultNewsListViewModel: NewsListViewModel{
func viewDidLoad() {
}
func didLoadNextPage() {
}
private(set) var currentPage: Int = 0
private var totalPageCount: Int = 1
var hasMorePages: Bool {
return currentPage < totalPageCount
}
var nextPage: Int {
guard hasMorePages else { return currentPage }
return currentPage + 1
}
private var newsLoadTask: Cancellable? { willSet { newsLoadTask?.cancel() } }
private let disposable = DisposeBag()
// MARK: - OUTPUT
let newsList: PublishSubject<NewsList> = PublishSubject()
let error: PublishSubject<String> = PublishSubject()
let loading: PublishSubject<Bool> = PublishSubject()
let isEmpty: PublishSubject<Bool> = PublishSubject()
func getNewsList() -> Void{
print("sono dentro il viewModel!")
NewsDataService.shared.getNewsList()
.subscribe { event in
switch event {
case .next(let progressResponse):
if progressResponse.response != nil {
do{
let json = try progressResponse.response?.map(NewsList.self)
print(json!)
self.newsList.onNext(json!)
}
catch _ {
print("error try")
}
} else {
print("Progress: \(progressResponse.progress)")
}
case .error( _): break
// handle the error
default:
break
}
}
}
}
This is my ViewController, where xCode give me the following error when i try to bind to tableNews:
Expression type 'Reactive<_>' is ambiguous without more context
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
#IBOutlet weak var tableNews: UITableView!
let viewModel = DefaultNewsListViewModel()
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
}
private func setupBindings() {
viewModel.newsList.bind(to: tableNews.rx.items(cellIdentifier: "Cell")) {
(index, repository: NewsList, cell) in
cell.textLabel?.text = repository.name
cell.detailTextLabel?.text = repository.url
}
.disposed(by: disposeBag)
}
}
This is the service that get data from API:
import Moya
import RxSwift
struct NewsDataService {
static let shared = NewsDataService()
private let disposable = DisposeBag()
private init() {}
fileprivate let newsListProvider = MoyaProvider<NewsService>()
func getNewsList() -> Observable<ProgressResponse> {
self.newsListProvider.rx.requestWithProgress(.readNewsList)
}
}
I'm new at rxSwift, I followed some documentation but i'd like to know if i'm approaching in the right way. Another point i'd like to know is how correctly bind my tableView to viewModel.
Thanks for the support.
As #FabioFelici mentioned in the comments, UITableView.rx.items(cellIdentifier:) is expecting to be bound to an Observable that contains an array of objects but your NewsListViewModel.newsList is an Observable<NewsList>.
This means you either have to extract the array out of NewsList (assuming it has one) through a map. As in newsList.map { $0.items }.bind(to:...
Also, your MoviesListViewModelOutput should not be full of Subjects, rather it should contain Observables. And I wouldn't bother with the protocols, struts are fine.
Also, your view model is still very imperative, not really in an Rx style. A well constructed Rx view model doesn't contain functions that are repeatedly called. It just has a constructor (or is itself just a single function.) You create it, bind to it and then you are done.

Mocking and Validating Results in RxSwift Unit Testing

I have just started learning RxSwift and trying to build a sample application to practice these concepts.
I have written a QuestionViewModel that loads list of questions from QuestionOps class. QuestionOps has a function getQuestions that returns Single<[Question]>.
Problem that I am facing is, how to mock the behavior of QuestionOps class while testing QuestionViewModel.
public class QuestionsListViewModel {
public var questionOps: QuestionOps!
private let disposeBag = DisposeBag()
private let items = BehaviorRelay<[QuestionItemViewModel]>(value: [])
public let loadNextPage = PublishSubject<Void>()
public var listItems: Driver<[QuestionItemViewModel]>
public init() {
listItems = items.asDriver(onErrorJustReturn: [])
loadNextPage
.flatMapFirst { self.questionOps.getQuestions() }
.map { $0.map { QuestionItemViewModel($0) } }
.bind(to: items)
.disposed(by: disposeBag)
}
}
public class QuestionOps {
public func getQuestions() -> Single<[Question]> {
return Single.create { event -> Disposable in
event(.success([]))
return Disposables.create()
}
}
}
I have created this MockQuestionOps for test purpose:
public class MockQuestionOps : QuestionOps {
//MARK: -
//MARK: Responses
public var getQuestionsResponse: Single<[Question]>?
public func getQuestions() -> Single<[Question]> {
self.getQuestionsResponse = Single.create { event -> Disposable in
return Disposables.create()
}
return self.getQuestionsResponse!
}
}
In my test case I am doing the following:
/// My idea here is to test in following maner:
/// - at some point user initates loading
/// - after some time got network response with status true
func testLoadedDataIsDisplayedCorrectly() {
scheduler = TestScheduler(initialClock: 0)
let questionsLoadedObserver = scheduler.createObserver([QuestionItemViewModel].self)
let qOps = MockQuestionOps()
vm = QuestionsListViewModel()
vm.questionOps = qOps
vm.listItems
.drive(questionsLoadedObserver)
.disposed(by: disposebag)
// User initiates load questions
scheduler.createColdObservable([.next(2, ())])
.bind(to: vm.loadNextPage)
.disposed(by: disposebag)
// Simulating question ops behaviour of responding
// to get question request
/// HERE: -----------
/// This is where I am stuck
/// How should I tell qOps to send particular response with delay
scheduler.start()
/// HERE: -----------
/// How can I test list is initialy empty
/// and after loading, data is correctly loaded
}
Here is a complete, compilable answer (not including imports.)
You tell qOps to emit by giving it a cold test observable that will emit the correct values.
You test the output by comparing the events gathered by the test observer with the expected results.
There is no notion of "the list is initially empty". The list is always empty. It emits values over time and what you are testing is whether it emitted the correct values.
class rx_sandboxTests: XCTestCase {
func testLoadedDataIsDisplayedCorrectly() {
let scheduler = TestScheduler(initialClock: 0)
let disposebag = DisposeBag()
let questionsLoadedObserver = scheduler.createObserver([QuestionItemViewModel].self)
let qOps = MockQuestionOps(scheduler: scheduler)
let vm = QuestionsListViewModel(questionOps: qOps)
vm.listItems
.drive(questionsLoadedObserver)
.disposed(by: disposebag)
scheduler.createColdObservable([.next(2, ())])
.bind(to: vm.loadNextPage)
.disposed(by: disposebag)
scheduler.start()
XCTAssertEqual(questionsLoadedObserver.events, [.next(12, [QuestionItemViewModel(), QuestionItemViewModel()])])
}
}
protocol QuestionOpsType {
func getQuestions() -> Single<[Question]>
}
struct MockQuestionOps: QuestionOpsType {
func getQuestions() -> Single<[Question]> {
return scheduler.createColdObservable([.next(10, [Question(), Question()]), .completed(10)]).asSingle()
}
let scheduler: TestScheduler
}
class QuestionsListViewModel {
let listItems: Driver<[QuestionItemViewModel]>
private let _loadNextPage = PublishSubject<Void>()
var loadNextPage: AnyObserver<Void> {
return _loadNextPage.asObserver()
}
init(questionOps: QuestionOpsType) {
listItems = _loadNextPage
.flatMapFirst { [questionOps] in
questionOps.getQuestions().asObservable()
}
.map { $0.map { QuestionItemViewModel($0) } }
.asDriver(onErrorJustReturn: [])
}
}
struct Question { }
struct QuestionItemViewModel: Equatable {
init() { }
init(_ question: Question) { }
}

Resources