How do I access an image URL that's not in the list JSON for my API in my Siesta Resource? - siesta-swift

I'm using Siesta with an API that returns a very lightweight list response for root entities. For instance, for /entity this is what the response looks like:
{
count: 200,
results: [
{
url: "https://example.com/api/entity/1/",
name: "foo"
},
{
url: "https://example.com/api/entity/2/",
name: "bar"
},
{
url: "https://example.com/api/entity/3/",
name: "bat"
}]
}
The full object found at the url in the results has an avatar property that I'd really like to show in my table view for this list but I can't figure out how to make that happen with the Siesta framework. Is it possible to fetch more details from the underlying /entity/1 endpoint as part of loading the resource for the /entity list?

In Siesta’s view of the world, one url ⟺ one resource. There is thus a “summary list” resource, /entity, plus a separate “entity detail” resource for each row, /entity/1 etc. It doesn’t matter that they happen to share some of the same data; Siesta itself doesn’t make any effort to merge, synchronize, prepopulate one resource from the other. Separate URLs, separate resources.
The rule of thumb is, “If you need data from a resource, observe that resource.” Since you want to use both summary info from /entities and detail info from /entities/n, you observe both resources.
Here is a sketch of an approach you might use:
Get your table view showing just the info from /entities, no avatars. You can use RepositoryListViewController from the example project as a starting point.
Make each table cell accept a summary model, and observe its corresponding detail resource:
class EntityTableViewCell: UITableViewCell, ResourceObserver {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var avatar: RemoteImageView!
private var summary: EntitySummary?
private var detailResource: Resource?
func showEntity(summary: EntitySummary) {
self.summary = summary
detailResource?.removeObservers(ownedBy: self)
detailResource = MyApi.resource(absoluteURL: summary?.url)
detailResource.addObserver(self).loadIfNeeded()
}
Now populate the cell in resourceChanged(), mixing and matching from the summary and detail as you see fit:
func resourceChanged(resource: Resource, event: ResourceEvent) {
let detail: EntityDetail? = detailResource?.typedContent()
nameLabel.text = detail?.name ?? summary?.name
avatar.imageURL = detail?.avatar
}
You might also want to stop observing when the cell moves out of view:
override func prepareForReuse() {
showEntity(nil)
}
}
(This sketch assumes that you have separate EntitySummary and EntityDetail models. You might also have a single Entity model with the detail-only fields optional, or you might just be using raw JSON dictionaries. The approach is the same regardless.)
Here’s what happens when a cell scrolls into view:
Your cellForRowAtIndexPath calls showEntity(_:), passing an EntitySummary it got from the /entities resource.
The cell starts observing /entities/n.
This immediate triggers resourceChanged(). The detail resource has no data yet, so your cell immediately gets populated with summary info only.
Eventually the detail resource loads. If your cell is still observing it, then resourceChanged() gets called again, and this time it sees the detail info.
Note that in #4, if your cell got scrolled out of view and reused before that detail resource loaded, then your cell will no longer be observing it — and thus the late-arriving response will not clobber the reused cell’s contents.

Related

How to add an item from another class? Swift

I’m doing a news management exercise. I have some classes:
Author
News (father)
NewsVideo (son) (this class has the same properties as the father, plus it has two properties)
NewsManagement
From the NewsManagement class, I have to create a function that inserts a news.
I have already created an empty array of type News.
NewsManagement class has only one property, an empty News array.
var newsCollection: [News] = []
class NewsManagement {
var news: [News] = []
init(news: [News]) {
self.news = news
}
func insertNews(title: String, text: String, date: Int, author: Autore, urlVideo: String?, videoLength: Double?) -> String? {
//these are the properties of the newsVideo class
guard let urlVideo = urlVideo, let videoLength = videoLength else {
return nil
}
self.news.append(News.init(title: String, text: String, date: Int, author: Author))
return "news addition”
}
}
var manager = NewsManagement.init(news: newsCollection)
manager.insertNews(title: "prima prova", text: "spero funzioni", date: 13, author: authors[3], urlVideo: "www.prova.it", videoLength: 4.5)
//authors is an array of instance of class Author
Unfortunately, it’s not working.
Do you know where the error is?
Or do you know another way?
I’ve been trying solutions for too long, but none of them work
Thank you!
Syntactic issues
There are a couple of syntax issues that prevent the code from compiling.
THere's a small typo with Autore instead of Author.
Then the bigger syntactical issue: replace the line which does not provide any value for the arguments:
self.news.append(News.init(title: String, text: String, date: Int, author: Author))
with a line that privides the named arguments:
self.news.append(News.init(title: title, text: text, date: date, author: author))
There is by the way no need to call explicitly init. And there's no need to use self when there is no ambiguity between a property and an argument. So you could further simplify:
news.append(News(title: title, text: text, date: date, author: author))
Not related: Design issues
The fact of providing all the parameters to create a News to the news manager is a design issue:
to add news, one must provide all the required parameters for all kind of news to the news manager.
the news manager has then not only the responsibility to manage the news collection, it suddenly has the additional responsibility to create each single News. As a consequence if a new kind of News is added (e.g. AudioNews) or if one of the news constructor would change the news manager would have to change too. More than one reason to change is a problem in view of the Single Responsibility Principle.
the news manager must also know the details of each constructor of each possible kind of news. ANd it has to know the rule which parameters create a which kind of news. So it needs to know much more about a lot of classes than really necessary. This could be seen as a problem in view of the Principle of Least Knowledge
Worse, the insertNews() has an interface that is cluttered with a lot of parameters that are not always needed. This is a problem in view of the interface segregation principle.
My advice: have News created separately using the right kind of news (e.g. VideoNews) with the appropriate constructor, and simplify NewsManager to insert, access and remove already existing news.

Why are subviews displaying old data when binding objects from an array?

Whenever a user returns to a form sub-view after saving data through a binding variable, the form displays old data from before it was edited, and I cannot understand why.
I have an array of Face objects (technically Core Data entities) declared as a State variable in a parent NavigationView. I then loop through them to create NavigationLinks like this:
#State var formDieFaces: [Face] = [] // this gets initialized akin to the below (truncated)
...
let face1 = Face(entity: Face.entity(), insertInto: nil)
self._formDieFaces = State(initialValue: [face1])
...
ForEach (formDieFaces.indices) { curIndex in
NavigationLink(
destination: FaceForm(self.$formDieFaces[curIndex]))
) {
FaceRowView(faceToList: self.formDieFaces[curIndex])
}
}
And in FaceForm, I receive the variable and "save" it as such:
#Binding var faceToEdit: Face
#State var formFaceText: String = String()
...
// in the form
TextField("Placeholder text", text: $formFaceText)
...
// on save, do the below
self.faceToEdit.text = self.formFaceText
What's weird is that on "saving" in FaceForm, the main NavigationView does update, and in that NavigationLink (FaceRowView) everything shows the correct data. But if I click back into FaceForm thereafter, it displays old data from before the save.
I have a feeling I'm doing something phenomenally obtuse with the State and Binding variables, but I just can't for the life of me figure out what's going on. I've tried various combinations of #ObservedObject, #ObservableObject, #StateObject, etc.
Could anyone please point me in the right direction?
Edit: to be clear, FaceRowView works and displays the correct data. FaceForm is where the problem is occurring, almost like it's remembering an old copy of the data.
If it's helpful, here is a link to a video of the behavior: https://youtu.be/8eC-TdtFP5s
For those curious, I'm not 100% sure this is a pure, proper SwiftUI solution, but I found a hack/workaround on this page that I've implemented and seems to have "solved" the problem: https://stackoverflow.com/a/57733708.

Is there a way to call a function on multiple subviews in a view?

I'm working on a weather app where I have subviews for each statistic ex: CurrentWeather, HourlyForecast, and DailyForecast are all separate views
and I am currently making a separate api call for each view but I know that this is very inefficient and I was wondering how I could call the func only once for everything. I have tried putting the func in a .onAppear on the "home screen" of my app which features all of the subviews but it doesn't seen to work as the data I need is being accessed in the subviews.
Any help would be very much appreciated as I am fairly new to SwiftUI
Here is a version of what I am basically doing right now:
#EnvironmentObject var data: WeatherAPI
VStack {
// Weather Condition and Temp
CurrentWeather().environmentObject(WeatherAPI())
//Hourly Forecast
HourlyModuleView()
//Weekly Forecast
ForecastModuleView().environmentObject(WeatherAPI())
}.onAppear(perform: data.loadData)
You should remove over-defines for environment objects and use one from, as I understood, HomeScreen view, because environment object injected into root view becomes automatically available for all subviews, like
#EnvironmentObject var data: WeatherAPI
...
VStack {
// Weather Condition and Temp
CurrentWeather() // << data injected automatically
//Hourly Forecast
HourlyModuleView() // << data injected automatically
//Weekly Forecast
ForecastModuleView() // << data injected automatically
}.onAppear(perform: data.loadData)

how do you iterate through elements in UI testing in Swift/iOS?

I understand how I could, for example look at one element in a table, but what's the correct way to simply iterate through all the elements, however many are found, to for example tap on them?
let indexFromTheQuery = tables.staticTexts.elementBoundByIndex(2)
Okay I succeeded in figuring out the simple syntax. I have just started working with Swift and so it took me sleeping on it to think of the answer.
This code works:
var elementLabels = [String]()
for i in 0..<tables.staticTexts.count {
elementLabels.append (tables.staticTexts.elementBoundByIndex(i).label)
}
print (elementLabels)
My guess is the answer is there is no way to do so.
The reason is because of my experience with one of the critical iOS components while making a number of UI tests: UIDatePicker.
If you record a test where you get the page up and then you spin the picker, you will notice that the commands are all non-specific and are screen related. In the case of the picker, however, community requests resulted in the addition of a method for doing tests: How to select a picker view item in an iOS UI test in Xcode?.
Maybe you can add a helper method to whatever controller contains this table. Also, keep in mind that you can easily add methods without polluting the class interface by defining them as extensions that are in test scope only.
For Xcode 11.2.1, SwiftUI and swift 5 based app, the following code works for testing a list, each element in this case appears as a button in the test code. The table is set up like this (for each row) :
NavigationLink(destination: TopicDetail(name: "Topic name", longDesc: "A description")) {
TopicRow(thisTopic: top).accessibility(identifier: "newTopicRow_\(top.name!)")
}
Then I catch the members of the table by getting the buttons into an array:
let myTable = app.tables.matching(identifier: "newTopicTable")
var elementLabels = [String]()
for i in 0..<myTable.buttons.count {
elementLabels.append (tablesQuery.buttons.element(boundBy: i).label)
}
print (elementLabels)
Finally, I deleted each member of the table by selecting the detail view where I have a delete button, again with
.accessibility(identifier: "deleteTopic"
I wanted to delete all members of the table:
for topicLabel in elementLabels {
let myButton = app.buttons[topicLabel]
myButton.firstMatch.tap()
app.buttons["deleteTopic"].tap()
}

Terms and Conditions View Controller for iOS

I was trying to make a simple Terms and Conditions view controller for my app. I was figuring a ScrollView with a Text Field.
What I am running into is displaying a really long formatted text string with multiple line feeds and quoted words and phrases.
NSString #termsAndConditions = #"Terms and Conditions ("Terms") ...
...
..."
How have others handled this? Since terms can change, how would one program this to be easily updated?
A code example would be really appreciated.
There is no one right way to ever do anything. What I have done in the past is to have the client or copywriter supply a word document. A word document is great for some one maintaining the content because it is easy for a non programmer. It also allows someone to easily specify some simple formatting for their terms of service content. Ie. bullet list, headers, links etc.
Then I would use an online doc2html converter to create an HTML file. (There are plenty of these online, a quick google can find one) Then I display the HTML file in a web view with in the terms of service or privacy policy view controller. If the app is using a specific font I just use a text editor to string replace the font declarations. Any extra styling requirements can be specified in a separate css file. For example, one that I just did required a specific colour for links to fit the theme of the app.
When it needs updating I can usually just do the edits in HTML if their are a few or regenerate it from a new Doc file.
Here is some example code of the view controller:
class TermsViewController: UIViewController {
#IBOutlet weak var termsWebView: UIWebView!
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
loadTermsHTML()
}
func loadTermsHTML() {
//Load the HTML file from resources
guard let path = NSBundle.mainBundle().
pathForResource("terms", ofType: "html") else {
return
}
let url = NSURL(fileURLWithPath: path)
if let data = NSData(contentsOfURL: url) {
termsWebView.loadHTMLString(NSString(data: data,
encoding: NSUTF8StringEncoding) as! String, baseURL: nil)
}
}
}

Resources