How to add network fetch inside swift struct initializer - uitableview

I'm trying to get data from an URL (1st network call), then make a 2nd network call to fetch image from the url in the first response, and present on tableview in swift.
The json from 1st network call contains 3 strings:
struct Item: Codable {
var title: String?
var image_url: String?
var content: String
}
What I need to do is, after json decoding and get this [Item] array, I also need to make network call for each item in it so I can also get the UIImage for each of them and present title, content and Image on each cell.
My question is, where is the best place to make the network call (in MVVM pattern)?
My first thought is in the TableViewCell:
func configCell(item: Item) {
titleLabel.text = item.title
descriptionLable.text = item.content
// fetch image
service.fetchImage(with: item.image_url) { result in
switch result {
case .success(let image):
DispatchQueue.main.async { [weak self] in
if let image = image {
self?.iconView.isHidden = false
}
}
case .failure(let error):
print(error.localizedDescription)
return
}
}
}
However, I'm not sure if this the right way/place to do so because it might cause wrong image attach to each cell. Also it couples network layer into view layer.
So my 2nd thought is, create a second model in the viewModel and make network call in it:
struct ImageItem: Codable {
var title: String?
var image_url: String?
var content: String
var image: Data?
init(with item: Item) {
self.title = item.title
self.content = item.content
ContentService().fetchImage(with: item.image_url) { result in
switch result {
case .success(let image):
self.image = image?.pngData()
case .failure(let error):
print(error.localizedDescription)
}
}
}
But that doesn't feel right either. Also seems like I can't make network call in struct initializer.
Could anyone help or give me advice about where to put this 2nd layer network call for fetching the image?

If those images are lightweight, you can do it directly inside cellForRow(at:) method of UITableViewDataSource. If using a library/pod such as Kingfisher - it's just 1 line that takes care of of, plus another one to import the library:
import Kingfisher
...
cell.iconView.kf.setImage(with: viewModel.image_url)
Otherwise, you may have to take care yourself of caching, not downloading images unless really needed etc.
As long as the image download/configuration is done in UIViewController and not inside the Custom Cell, it should be all good. But still make sure you use some protocol and separate Repository class implementing that protocol and containing the image download functionality, because the View Controller should be more like a Mediator and not be loaded with too much code such as networking and business logic (VCs should act more like view, and generally speaking it's not their job to get the remote image for your custom cells, but for simpler apps that should be ok, unless you want to over-engineer things :) ).
Some people argue that View Models should be light weight and therefore structs, while others make them classes and add lots of functionality such as interacting with external service. It all depends of what variation of MVVM you are using. Personally I prefer to simply keep a URL property in the View Model and implement the "getRemoteImage(url:)" part outside the custom cell, but I've seen people adding UIImage properties directly to the View Models or Data type properties and having some converters injected that transform Data -> UIImage.
You can actually have a Repository and inject the Networking code to the View Model, and then add an Observer<UIImage?> inside the View Model and bind the corresponding cells' images to those properties via closures so the cell automatically updates itself on successful download (if still visible) or next time it appears on screen... Since each cell would have a corresponding view model - each individual view model can keep track if the image for IndexPath was already dowloaded or not.

Related

SwiftUI MVVM approach with vars in View

I'm building an app in SwiftUI, based on the MVVM design pattern. What I'm doing is this:
struct AddInvestment: View {
#ObservedObject private var model = AddInvestmentVM()
#State private var action: AssetAction?
#State private var description: String = ""
#State private var amount: String = ""
#State private var costPerShare: String = ""
#State private var searchText: String = ""
#State var asset: Asset?
var body: some View {
NavigationView {
VStack {
Form {
Section("Asset") {
NavigationLink(model.chosenAsset?.completeName ?? "Scegli asset") {
AssetsSearchView()
}
}
Section {
Picker(action?.name ?? "", selection: $action) {
ForEach(model.assetsActions) { action in
Text(action.name).tag(action as? AssetAction)
}
}
.pickerStyle(.segmented)
.listRowBackground(Color.clear)
}
Section {
TextField("", text: $amount, prompt: Text("Unità"))
.keyboardType(UIKit.UIKeyboardType.decimalPad)
TextField("", text: $costPerShare, prompt: Text("Prezzo per unità"))
.keyboardType(UIKit.UIKeyboardType.decimalPad)
}
}
}
.navigationTitle("Aggiungi Investimento")
}
.environmentObject(model)
.onAppear {
model.fetchBaseData()
}
}
}
Then I have my ViewModel, this:
class AddInvestmentVM: ObservableObject {
private let airtableApiKey = "..."
private let airtableBaseID = "..."
private let assetsTableName = "..."
private let assetsActionsTableName = "..."
private let airtable = Airtable.init("...")
private var tasks = [AnyCancellable]()
#Published var chosenAsset: Asset?
#Published var assets = [Asset]()
#Published var assetsActions = [AssetAction]()
init() { }
func fetchBaseData() {
print("Fetching data...")
let assetActionsRequest = AirtableRequest.init(baseID: airtableBaseID, tableName: assetsActionsTableName, view: nil)
let assetsActionsPublisher: AnyPublisher<[AssetAction], AirtableError> = airtable.fetchRecords(request: assetActionsRequest)
assetsActionsPublisher
.eraseToAnyPublisher()
.sink { completion in
print("** **")
print(completion)
} receiveValue: { assetsActions in
print("** **")
print(assetsActions)
self.assetsActions = assetsActions
}
.store(in: &tasks)
}
}
Now, as you can see I have some properties on the view that are binded to some text fields. Let's take these in consideration:
#State private var description: String = ""
#State private var amount: String = ""
#State private var costPerShare: String = ""
#State private var searchText: String = ""
Keeping in mind the MVVM pattern, should these properties be declared in the ViewModel and binded from there? Or is this a right approach?
Instead of giving you a concrete answer which concrete variable you might move to the view model, I would like to give you a more general answer, which might also help you in the decision of other use cases, and eventually should help to answer your question yourself ;)
A View Model publishes the binding (I am not talking about a #Binding here!), which completely and unambiguously describes what a view shall render. The how it actually looks like may be still part of the view.
Tip: define a struct which contains all the variables constituting the binding, then publish this struct in your view model. You may name this binding ViewState.
If we take this strict, it means in other words, that for each possible value of the binding, there is one and only one visual representation of the view.
However, in practice, it is favourable to relax this a bit, otherwise you would have to specify and handle even the scrolling position of a scroll view, too. How far you go with this may depend on the complexity of your view and the whole scenario. But the rule, that the binding is the definitive source of truth for the view and determines what it is rendering should not be violated.
Generally, in a scenario where your model is not mutable (i.e. there are no user actions which alter the model), you very rarely would use #State variables in a SwiftUI view, since there is nothing which is variable.
Even when it happens that the elements in the list change or the order or the number of elements change, the list view always receives a constant array of elements whose elements are also not mutable by the user. Thus, you use a let elements: [Element] in your SwiftUI view.
On the other hand, your view model may use a model which publishes a value of [Element] and the view model observes it, and then mutates the binding accordingly.
Another principle of MVVM is (actually any MV* pattern) that your view does not perform logic on its own and it never ever changes the binding.
Instead, your view signals the user's "intent" via call backs (aka actions, events, intents) which eventually will reach the view model and then handled there, which in turn may eventually affect the binding.
Strictly, that would mean, if you have a scenario where a user can edit a string, and IFF you use a binding for this string, the string cannot be mutated by editing of the user. Instead, each editing command will be send to the view model, the view model then handles the changes, and sends back the mutated binding which eventually will reflect the user's changes.
Also, in practice there might be more favourable solutions: a view might use its own "edit buffer" for the string, using a #State variable. This string gets mutated "internally" within the view when the user edits the text. The model only gets informed when the user "commits" the changes, or when the user taps the "back" or "done" button. This approach is favourable when the "editing" itself is simple and no complex validation is required during editing, or when there is no need to trigger side effects (like updating a suggestion list in a search bar). Otherwise, you would route the editing through the view model.
Again in practice, you decide how far you go with a view that has "it's own behaviour". It depends on the use case and what is favourable in terms of avoiding unnecessary complex solutions which don't make the solution any better through being too strict to the pattern.
Conclusion:
In MVVM, you use #State variables in Views only iff your View adds a behaviour which does not strictly need to be monitored by the View Model and does not violate the rule that the Binding is the definitive source of truth from the perspective of the view and that the View Model still is the authority for the performed logic.
I tend to think of the View as being the definition of the human interface of an app for a device or set of devices (iPhone, iPad, Mac, etc). I tend to think of the Model as the place where the applications logic goes that is not part of any View (business logic, data manipulation, network manipulation, etc). I think of the ViewModel as being the place where any logic exits that the View needs to make the View work, such as formatting data for display on the View. I think you are looking for where the single source of truth lives for a data element needed by the View. To my thinking it should only live in the ViewModel if and only if it is specific to the View. Any logic or data that is about the application should live in the Model, not the ViewModel. By doing this I make my Model more reusable, more independent of the View. There are always borderline cases and I think about if this data or logic would need to be recreated if I built an entirely separate second View, say one that worked with a Browser via HTML/JavaScript/Etc. If I would have to recreate that data or logic with a new View layer, then I think the data or logic belongs in the Model. If the data or logic is specific to this View layer, then I would put it in the ViewModel for that View.
Using the above thinking your AddInvestment class looks like something I would put in the Model, not the ViewModel. Of course, it is all a matter of taste and I do not think there is any one right answer here, but that is how I think I would slice and dice it in my own apps.

Store label information into history table | Swift | Xcode

Store label information into history table | Swift | Xcode
If someone can help me with this. So basically what this application does is, it uses azure cognitive services to convert a picture into text and display it in the second screen when analyze is pressed, I want this information to be stored in the third view as a history. Can anyone help in achieving this? Very new in swift, so trying to figure out stuff. Thanks in advance.
Screenshot of the app main storyboard, to clarify what I am asking:
So all you really need is a data structure that allows you to store your data and allow it to be passed between view controllers. The simple approach would be to have:
struct PictureDetail {
photo: UIImage
text: String
}
Depending on the volume/size of images, you'd probably better actually holding the photos as files and just keeping the file path in the struct and have a calculated property that retrieves it from disk:
struct PictureDetail {
photoPath: URL
text: String
photo: UIImage {
// load photo from URL & return
}
}
The you need some form of collection to hold all your data and to allow it to be injected into wherever it is needed
class DataModel {
var pictureData: [PictureDetail] = []
func addPictureDetail(picture: UIImage, text: String) {
//save picture to disk and obtain URL
pictureData.append(PictureDetail(url: url, text: text)
}
Then instantiate the data model in your initial view controller
let dataModel = DataModel()
And then have a DataModel property in the other VCs, and inject the value during your prepare(for segue: IUSegue) method.

Swift: Can I load Url data inside a collection/table view cell?

I have a horizontal scrolling collection view inside a UITableView cell, achieving the view same as that of Netflix.
Currently, I am loading URL data in my view controller containing table view and passing the array of data in UITableViewCell which contains the collectionView, and then rendering collection view cells.
But I'm feeling lack of controls using this method. For e.g, UI management, hiding, showing views depending on URL data load and error, etc.
I tried loading URL data inside table view cell and that works
perfectly fine for me but I don't think that's appropriate to do, as
only controllers should control everything.
The closure I'm using to load data in my controller is -
private func fetchData() {
let id = UserDefaults.standard.getUserId()
Service.shared.fetch(userId: id) { (data, error) in
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: {
guard error == nil else {
print(error?.localizedDescription ?? "Error")
return
}
let result = data?.count != 0 ? "Success" : "Failure"
switch result {
case ResultType.Failure.rawValue:
print("Failure")
case ResultType.Success.rawValue:
if let data = data {
self.data = data
}
default: break
}
})
}
}
Back to the Question, Is it fine loading the data inside
UITableViewCell in order to hide/show or animate UICollectionView inside that UITableViewCell?
Moreover, Assume a scenario where I have to load 4-5 URL data and render them in each custom table view cell which may or may not contain a collection view.
Complicated!
You can load data in a view, but it wouldn’t be a good architecture. It hampers reusabilty and mixes responsibilities. Also, table view cells will be reused, which will eventually lead to weird data loading behavior and probably bugs.
I suggest extracting data loading into a custom class, and using that class in the view controller. This way your data loading is decoupled from the controller and the view, giving the most flexibility.

How to use an array over multiple view controllers?

I have been playing around with a lot of stuff involving arrays and scrollviews. I have mostly stayed within the confines of view controllers, so usually i'll grab data from firebase, add it to an array, and then send it to the tableview or collectionview. What I'm trying to do now is actually navigate between viewcontrollers (or multiple copies of the same view controller) and applying the array items to each view controller.
For example I want to be able to grab some photos from firebase, put them in an array of url strings or whatever. Then I want to put a photo on the background of a view controller. Then when I push the over button it goes navigates to the next view controller and puts the next photo as the background there, etc.
I understand there are probably multiple ways to do this and I was wondering what is the most efficient way? Do I just put an array in a Global class and access that from all the view controllers? Or do I just keep the array in the first view controller, then as I navigate, keep sending it to the next view controller over and over? Also there will be a LOT of items and objects and arrays here so that's why I'm looking for efficiency. Thanks in advance to anyone able to help with this, and I hope I explained it well enough!
This is a very simple way of adding and retrieving String value from a struct, here you are saving the image url string as a value in a dictionary and it's key is going to be the ViewController name.
struct SavedData {
static private var imagesDictionary: [String: String] = [:]
static func image(for viewController: UIViewController) -> String? {
return imagesDictionary["\(type(of: viewController))"]
}
static func add(image name: String, for viewController: UIViewController) {
self.imagesDictionary["\(type(of: viewController))"] = name
}
}
saving a value is very simple, if you're saving the data in a viewController and you want a specific image to be saved for that viewController you can use self
SavedData.add(image: "img1.png", for: self)
And if you want to save an image for a different viewController, do it like this.
SavedData.add(image: "img2.png", for: SecondViewController())
Retrieving the image is also very simple, you should call this method in the viewController that you want to assign the image to.
let savedImage = SavedData.image(for: self)
print(savedImage!)

Passing data between views via segmented bar

I know this is a frequently asked question throughout the forums and I hate to ask another seemingly simplistic question, but I can't manage to find a solution on data passing in regards to my specific circumstance.
Basically, I have a view controller embedded within a navigation controller; of which displays a segmented bar, acting as a 'profile selector'. After selection, I want a series of images on another view to be changed after different profiles are selected, but the data passing isn't seem to be working whatsoever. I'm unsure if delegate is required within my specific circumstance.
Essentially; I'd just love an example of how to pass a case value for a segmented bar so I would be able to perform a simple case statement like follows: (where the case values have been passed from the previous view controller)
#IBAction func chooseImage(sender: AnyObject) {
switch myPhotoSegment.selectedSegmentIndex {
//if first segment selected
case 0:
//stop image animation if currently animating
newImageView.stopAnimating()
//update displayed image
newImageView.image = UIImage(named: "1.jpg")
//if second segment selected
case 1:
//stop image animation if currently animating
newImageView.stopAnimating()
//update displayed image
newImageView.image = UIImage(named: "2.jpg")
//if third segment selected
case 2:
//stop image animation if currently animating
newImageView.stopAnimating()
//update displayed image
newImageView.image = UIImage(named: "3.jpg")
//if fourth segment selected
case 3:
//stop image animation if currently animating
newImageView.stopAnimating()
//update displayed image
newImageView.image = UIImage(named: "4.jpg")
//by default, no segment selected
default:
newImageView.image = nil
}
}
I know my problem is long and probably poorly explained, but any feedback would be greatly appreciated. I kind of struggle with the whole logic and understanding of passing data, so if you could break down the solution for me as simply as possible; that would be incredible.
To share data between those two classes they either need to know about each other or know about a shared object. For example, you could create a singleton data model that contains the properties you want to pass back and forth.
private let instance = MySingleton()
class MySingleton {
var somethingImInterestedIn: String?
var sharedInstance: MySingleton {
get {
return instance
}
}
}
Each class would get a reference to this by using:
MySingletion.sharedInstance
Once they have the reference to the singleton they can set or get any of the properties in that object. For your case you would want to store an enum instead of a String.

Resources