In SwiftUI, I'm fetching objects via API into an optional array. In a view, I'm then trying to determine if that array contains a set of possible elements when the view is loaded.
If the array contains that element, I'll show a separate view for it. If it doesn't I won't show it.
How do I check if that optional array contains a specific element?
So for example, I want to check if projectType.landscapes? contains a "mulching" entry. Also, the ProjectType.Landscapes.types array will contain string values that correspond to the present projectType.landscape options, for example, ["mulching", "clearing", "treeCare"] or simply ["mulching"].
Here's my data structure:
// MARK: - ProjectType
struct ProjectType: Codable {
let landscapes: [Landscape]?
let generalConstructions: [GeneralConstruction]?
}
// MARK: - GeneralConstruction
struct GeneralConstruction: Codable {
let id: Int
}
// MARK: - Landscape
struct Landscape: Codable {
let id: Int
let types: [String]
let mulching: Mulching?
let clearing: Clearing?
let planting: Planting?
let treeCare: TreeCare?
let other: Other?
}
// MARK: - Clearing
struct Clearing: Codable {
let id: Int
let clearingArea, removeTrees, treeCount, approxTreeHeight: String
let treeStumps: String
}
// MARK: - Mulching
struct Mulching: Codable {
let id: Int
let mulchingType, currentlyInPlace, currentlyInPlaceCustom, roomLength: String
let roomWidth, color, customColor, length: String
let approximateLength: String
}
// MARK: - Other
struct Other: Codable {
let id: Int
let otherDetails: String
}
// MARK: - Planting
struct Planting: Codable {
let id: Int
let designPlan, treeLargerThanFiveFeet, plantCount: String
}
// MARK: - TreeCare
struct TreeCare: Codable {
let id: Int
let careDescription: String
}
Here's where I'd check to see if the landscape types are present:
import SwiftUI
struct LandscapeSpecificsView: View {
let projectType: ProjectType
var body: some View {
VStack{
Text("Here is the landscape specific project info. I'll show mulching or clearing, or treeCare, etc.")
}
}
}
struct MulchingView: View {
var body: some View {
Text("Here is the mulching info")
}
}
struct ClearingView: View {
var body: some View {
Text("Here is the clearing info")
}
}
struct PlantingView: View {
var body: some View {
Text("Here is the planting info")
}
}
struct TreeCareView: View {
var body: some View {
Text("Here is the treecare info")
}
}
struct LandscapeOtherView: View {
var body: some View {
Text("Here is the landscape other info")
}
}
first, you can use 'optional binding' methods like ' if let' or 'guard let' or simply using array count to make sure your array has a value.
after that you can use several options on Array like 'contains(_:)'
apple document
if let landScapes = project.landscapes {
if landScapes.contains(where: {$0.mulching != nil}) {
}
}
Related
I have an array of Struct
struct StartView: View {
struct ExampleStruct {
var name: String
var keywords: String
var destinationID: String
init(name: String, keywords: String, destinationID: String) {
self.name = name
self.keywords = keywords
self.destinationID = destinationID
}
}
// Created 3 examplmes
let AStruct = ExampleStruct(name: "Structure A",keywords: "first: school",destinationID: "SAID")
let BStruct = ExampleStruct(name: "Structure B",keywords: "second: church",destinationID: "SBID")
let CStruct = ExampleStruct(name: "Structure C",keywords: "third: bank",destinationID: "SCID")
}
// Created my array of structures
var StructureArray = (AStruct, BStruct, CStruct)
However, I am now trying to create a NavigationView with a List but I am having issues getting this to work.
var body: some View {
NavigationView{
List {
for index in StructureArray.indices {
VStack {
// Text to put ExampleStruct name
// Text to put ExampleStruct keywords
// Text to put ExampleStruct destinationID
}
.padding()
}
}
.navigationTitle("Structure Search")
}
}
I get a Closure containing control flow statement cannot be used with result builder 'ViewBuilder' based on the for index in StructureArray.indices
I have also tried:
for index in 0..<StructureArray.count {
let currentStruc = StructureArray[index]
HStack {
Text (currentStruc.name)
Text (currentStruc.keywords)
\\ etc
}
But get the same error. I have searched online for the last few hours and am still lost on how to make this work. Am I missing something obvious? Would a ForEach be better?
You can try like below. I have corrected a few of your code. Google the changes to better understand it.
ExampleStruct
struct ExampleStruct: Identifiable {
let id = UUID()
var name: String
var keywords: String
var destinationID: String
}
ContentView
import SwiftUI
struct ContentView: View {
let aStruct = ExampleStruct(name: "Structure A",keywords: "first: school",destinationID: "SAID")
let bStruct = ExampleStruct(name: "Structure B",keywords: "second: church",destinationID: "SBID")
let cStruct = ExampleStruct(name: "Structure C",keywords: "third: bank",destinationID: "SCID")
var structArray: [ExampleStruct] {
return [aStruct, bStruct, cStruct]
}
var body: some View {
List {
ForEach(structArray) { item in
Text(item.name)
}
}
}
}
Playgrounds test
I am attempting to build a basic app that returns a list of news items from a newsapi.org API data source. I set up my viewModel to fetch news from the API and convert the data to the model. I then set up my model to be structured around specific items from the api response (see URL for API in the viewModel below). Finally, I set up my ContentView to return the news items in a list. The app builds fine, but the news items (source name, article title) do not populate on the screen, and in the console I get the printed "failed" message. Is my model not set up correctly? Should Article instead be an array of Article (ex. [Article])? Any idea how to set up the model (or viewModel) in order to populate news items on the screen? I appreciate the feedback.
ContentView
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = NewsViewModel()
var body: some View {
NavigationView {
VStack {
List(viewModel.articles, id: \.articles.source.id) { news in
VStack(alignment: .leading) {
Text(news.articles.source.name)
Text(news.articles.title)
Text(news.articles.description)
}
}
}
.navigationTitle("News List")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Model
import Foundation
struct APIResponse: Codable {
let articles: Article
}
struct Article: Codable {
let source: Source
let title: String
let description: String
}
struct Source: Codable, Identifiable {
let id: String
let name: String
}
ViewModel
import Foundation
class NewsViewModel: ObservableObject {
#Published var articles = [APIResponse]()
init() {
fetchNews()
}
func fetchNews() {
guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=4b6cfa9b54c74b4db7d7d8a2120718d3") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else {
return
}
do {
let model = try JSONDecoder().decode([APIResponse].self, from: data)
//update properties on the main thread
DispatchQueue.main.async {
self.articles = model
}
}
catch {
print("failed")
}
}
task.resume()
}
}
Always print error in case you aren't sure why its failing:
print("\(error)") // Instead of print("failed")
Make it optional if any value has null in it:
struct APIResponse: Codable {
let articles: [Article]
}
struct Article: Codable {
let source: Source
let title: String
let description: String?
}
struct Source: Codable, Identifiable {
let id: String?
let name: String?
}
Update the view:
struct ContentView: View {
#ObservedObject var viewModel = NewsViewModel()
var body: some View {
NavigationView {
List(viewModel.articles, id: \.source.id) { news in
VStack {
VStack(alignment: .leading) {
Text(news.source.name ?? "")
Text(news.title)
Text(news.description ?? "")
}
}
}
.navigationTitle("Landmarks")
}
}
}
View Model Updates:
#Published var articles = [Article]()
let model = try JSONDecoder().decode(APIResponse.self, from: data)
//update properties on the main thread
DispatchQueue.main.async {
self.articles = model.articles
}
I am using MVVM in my SwiftUI project and after I request to fetch data from the API, I want to loop through the data and store some of them inside an array, however it returns error, what is the correct method to do this?
Here is my code and data struct
MenuDetailView
struct MenuDetailView: View {
#ObservedObject var viewModel = MenuDetailViewModel()
var body: some View {
ForEach(self.viewModel.variantGroup, id: \.self) { vg in
/*** I Need to loop and store vg.variantGroupName into array viewModel.variantChosen, how? ***/
self.viewModel.variantChosen.append(vg.VariantGroupName)
// This always return error:
// Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
VStack {
HStack {
Text(vg.variantGroupName)
Text(String(self.viewModel.arrVariantChoosen[0]))
}
VStack {
ForEach(vg.variant, id: \.self) { v in
Text(v.variantName)
}
}
}
}
}
}
}
MenuDetailViewModel.swift
class MenuDetailViewModel: ObservableObject, MenuDetailService {
var apiSession: APIService
#Published var detaildata: MenuDetailData?
#Published var variantGroup = [MenuDetailVariantGroup]()
#Published var variantChosen: Array<String> = []
var cancellables = Set<AnyCancellable>()
init(apiSession: APIService = APISession()) {
self.apiSession = apiSession
}
func getMenuDetail() {
let cancellable = self.getMenuDetail()
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print("Handle error: \(error)")
case .finished:
break
}
}) { (detail) in
self.detaildata = detail.data
self.variantGroup = detail.data.variantGroup
}
cancellables.insert(cancellable)
}
}
MenuDetailData.swift
struct MenuDetailData: Codable, Identifiable {
let id = UUID()
let idMenu: String
let menuName: String
let variantGroup: [MenuDetailVariantGroup]
}
MenuDetailVariantGroup.swift
struct MenuDetailVariantGroup: Codable, Identifiable, Hashable {
let id = UUID()
let variantGroupName: String
let variant: [MenuDetailVariant]
let limit: Int
}
MenuDetailVariant.swift
struct MenuDetailVariant: Codable, Identifiable, Hashable {
let id = UUID()
let variantName: String
}
Thank you all in advance
You can not add this inside the ForEach. In SwiftUI, ForEach is a view. It accepts view data, it's not the same as Array.forEach.
You need to do it inside the view model. Like this
func getMenuDetail() {
let cancellable = self.getMenuDetail()
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print("Handle error: \(error)")
case .finished:
break
}
}) { (detail) in
self.detaildata = detail.data
self.variantGroup = detail.data.variantGroup
self.variantChosen = self.variantGroup.map{$0.variantGroupName} //<--here
}
cancellables.insert(cancellable)
}
Remove this from ForEach
self.viewModel.variantChosen.append(vg.VariantGroupName)
I have this model:
struct Training: Identifiable {
let id = UUID()
let name: String
let workout: [Workout]?
}
and:
struct Workout: Identifiable {
let id = UUID()
let name: String
let exercices: [Exercice]?
}
and:
struct Exercice: Identifiable {
let id = UUID()
let name: String
}
The data for the models is coming from an environment object.
The app will launch with an empty list of trainings and you can add trainings within the UI. Each training has a navigtaionlink to a view to add workouts to each training and in the next step you can add exercices to each workout.
In my logic I create multidimensional arrays with the structs shown above.
The trainings view is easy:
struct TrainingsView: View {
#EnvironmentObject var appState: AppState
#State var showingDetail = false
var body: some View {
NavigationView {
VStack {
List {
ForEach (appState.trainings) { training in
NavigationLink(destination: WorkoutsView(training: training).environmentObject(self.appState)) {
Text(training.name)
}
}
.onDelete(perform: appState.removeTraining)
}
// Button to add trainings....
.navigationBarTitle(Text("Trainings").foregroundColor(Color.white))
}
}
}
}
The WorkoutsView is looking the same but I have an issue with listing the items of the parent training:
struct WorkoutsView: View {
// ...
var training: Training
var body: some View {
VStack {
List {
ForEach (appState.trainings(training).workouts) { workout in // I know the appState call is incorrect, but I don't know how to access is correctly.
NavigationLink(destination: ExercicesView(workout)) {
Text(workout.name)
}
}
}
// ...
}
}
}
I already tried:
List {
ForEach (0 ..< appState.trainings.count) {
NavigationLink(destination: WorkoutsView(training: $0).environmentObject(self.appState)) {
Text(appState.trainings[$0].name)
}
}
}
I could use appState.trainings[training].workouts in the WorkoutsView but I'm getting the error Contextual closure type '() -> Text' expects 0 arguments, but 1 was used in closure body on the NavigationLink line and don't know what to do.
Additional question: If this is close to the solution, I don't need the struct to conform to Identifiable?
You have 2 broad approaches here, depending on how you want to design your system.
1. Your child views know about the app state and can modify it directly. So, the parent needs to pass the indices/keys for the child to locate which data to modify:
struct TrainingsView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
List(0..<appState.trainings.count) { i in
NavigationLink(destination: WorkoutsView(trainingIndex: i)) {
Text(self.appState.trainings[i].name)
}
}
}
}
}
struct WorkoutsView: View {
var trainingIdx: Int
#EnvironmentObject var appState: AppState
var body: some View {
VStack() {
TextField("training name: ", text: $appState.trainings[trainingIdx].name)
Button(action: {self.appState.trainings[trainingIdx].workouts.append(Workout(...))}) {
Text("Add workout")
}
}
}
}
2. Alternatively, you might say that you don't want your child views to know about the app's state - you just want them to modify some static struct that they don't own (but rather owned by their parent), then you should use use a #Binding.
The example below is conceptual to illustrate a point:
struct TrainingsView: View {
#State var trainingA = Training(...)
#State var trainingB = Training(...)
var body: some Body {
NavigationView {
List {
WorkoutsView(training: $trainingA)
WorkoutsView(training: $trainingB)
}
}
}
}
struct WorkoutsView: View {
#Binding var training: Training
var body: some View {
VStack() {
TextField("training name: ", text: $training.name)
Button(action: { self.training.workouts.append(Workout(...)) }) {
Text("Add workout")
}
}
}
}
I am making a collection view that shows somebody their 'feed' which includes their compliments, likes, replies, and sent. I have an object named Feed which is made of an array of compliments, an array of likes, and array of replies, and an array of sent. Every object within those smaller arrays has a date_created property.
I know how to fill a collection view based off of one array sorted by dates, but how can I load a collection view with every like, reply, compliment, and sent from four arrays based on their date sent?
Do I perhaps append every one of these objects to one array? If so, how can I then go through that array and sort everything in it?
struct Feed: Decodable {
let likes: [Like]
let replies: [Reply]
let compliments: [Compliment]
let sent: [Compliment]
}
struct Like: Decodable {
let id: Int
let user: QuickUser
let created_at: String
}
struct Reply: Decodable {
let parent: Compliment
let content: String
let created_at: String
}
struct Compliment: Decodable {
let id: Int
let content: String
let sender: QuickUser
let created_at: String
let is_liked: Bool
let recipient: QuickUser
let is_public: Bool
}
func getFeed() {
UserServices.getInboxFeed { feed in
guard let feed = feed else {
self.showOkayAlert(title: "Oops!", message: "We are having trouble loading your inbox.", completion: nil)
return
}
self.feed = feed
DispatchQueue.main.async {
//load collection view here
}
}
}
A mockup of what the collection view will look like
Well, to solve this i have made a dummy protocol that contains the shared properties they all share.
in this case it was created_at and i assume this should be in date format.
however the next step was to make an extension of Feed that contains a function
that returns the latest object that confirm to the dummy protocol that i called, it LatestAction,
it basically sort each array by date and return the first item.
in our case the latest date,
after that creates a [LatestAction] sort it again the same way and return the first item which is the LatestAction.
Observe the code below.
struct QuickUser: Codable {
}
struct Feed: Decodable {
let likes: [Like]
let replies: [Reply]
let compliments: [Compliment]
let sent: [Compliment]
}
extension Feed {
func latestAction() -> LatestAction {
let latestLike = self.likes.sorted(by: { $0.created_at.compare($1.created_at) == .orderedDescending }).first!
let latestReply = self.replies.sorted(by: { $0.created_at.compare($1.created_at) == .orderedDescending }).first!
let latestComp = self.compliments.sorted(by: { $0.created_at.compare($1.created_at) == .orderedDescending }).first!
let latestActions: [LatestAction] = [latestReply, latestLike, latestComp].sorted(by: { $0.created_at.compare($1.created_at) == .orderedDescending })
return latestActions.first!
}
}
protocol LatestAction {
var created_at: Date { get }
}
struct Like: Decodable, LatestAction {
let id: Int
let user: QuickUser
let created_at: Date
}
struct Reply: Decodable, LatestAction {
let parent: Compliment
let content: String
let created_at: Date
}
struct Compliment: Decodable, LatestAction {
let id: Int
let content: String
let sender: QuickUser
let created_at: Date
let is_liked: Bool
let recipient: QuickUser
let is_public: Bool
}
// Simply used like this
var latest = feed.latestAction()
We are not done yet if you checked the latest type it would say LatestAction and only can access the one property in there which is created_at,
now the for the last part we are going to TypeCast the Object.
if let latest = feed.latestAction() as? Like {
// used as Like
} else if let latest = feed.latestAction() as? Compliment {
// used as Compliment
} else if let latest = feed.latestAction() as? Reply {
// used as reply
}