Swift HStack View not conforming to protocol 'View' - swift

I have built this code
struct StarDifficultyView: View {
var numberOfStarsToShow: Int
var numberOfTotalStarsToShow: Int = 5
var body: some View {
HStack{
var numberLeftToShow = numberOfStarsToShow
ForEach(1..<numberOfTotalStarsToShow+1){_ in
if(numberLeftToShow > 0){
Image(systemName: "star.fill")
.foregroundColor(Color.yellow)
numberLeftToShow -= 1
}else{
Image(systemName: "star.fille")
.foregroundColor(Color.yellow)
}
}
}
}
}
It gives me an error on the line if(numberLeftToShow > 0){ saying "Type '()' cannot conform to 'View'"
Can anyone tell me what I'm doing wrong

Explaining the issue:
You should not add expressions inside the view builder. So numberLeftToShow -= 1 will throw and error because it returns a void ('aka' type()) and this does not conform to View! that is the exact reason for the compiler!
Note 1
Don't use SwiftUI like the UIKit! SwiftUI views may execute over time on any state change and should not be used for calculating anything in this way
Note 2
You can convert 1..<numberOfTotalStarsToShow+1 to a closed range like 1...numberOfTotalStarsToShow (Although you don't need it at all for this question)
Note 3
Try not to use branch and convert your if/else code to something like:
Image(systemName: numberLeftToShow > 0 ? "star.fill" : "star.fille")
.foregroundColor(Color.yellow)
Note 4:
The lower bound of a range can not be less than the upper range, but you can iterate over a reversed range like:
(1...numberOfTotalStarsToShow).reversed()
Note 5:
try using a single source of truth like the forEach parameter itself!
Note 6:
Swift can infer the type and you don't need to pass it again:
so change Color.yellow to .yellow
Final Result:
Here is the code reviewed answer (based on the answer you have provided yourself):
var body: some View {
HStack {
ForEach(1...numberOfTotalStarsToShow, id:\.self) { i in
Image(systemName: "star.fill")
.foregroundColor(i > numberOfStarsToShow ? .gray : .yellow)
}
}
}

Don't throw away the closure parameter for the ForEach!
var body: some View {
HStack{
ForEach(0..<numberOfTotalStarsToShow){ i in // don't ignore the "i" here by writing "_"
// "i" will be different in each "iteration"
// use that to figure out which image to show
if(i < numberOfStarsToShow){
Image(systemName: "star.fill")
.foregroundColor(Color.yellow)
} else {
Image(systemName: "star")
.foregroundColor(Color.yellow)
}
}
}
}

Never mind, I just did this
struct StarDifficultyView: View {
var numberOfStarsToShow: Int
var numberOfTotalStarsToShow: Int = 5
var body: some View {
HStack{
ForEach(1..<numberOfStarsToShow+1){_ in
Image(systemName: "star.fill")
.foregroundColor(Color.yellow)
}
ForEach(1..<numberOfTotalStarsToShow-numberOfStarsToShow+1){_ in
Image(systemName: "star.fill")
.foregroundColor(Color.gray)
.opacity(0.7)
}
}
}
}
Basically, it just loops through the number of yellow stars to show and then works out how many grey ones to show and does another ForEach to display the leftover ones needed

Related

How to implement infinite swipable week view in SwiftUI?

I am currently working on an app with class schedule. I need to implement a swipable week view just like in Apple's "Calendar" app. There is no built-in solution for this in SwiftUI (Maybe I didn't find it), so I used SwiftUIPager library. It kinda works, but I have to provide it an array of elements. Then Pager use this elements to create pages. This does not suit me, so I decided to dynamically add elements of the array when I approach its border.
struct WeekObserverView: View {
let vm: WeekObserverViewModel = WeekObserverViewModel()
let OnDayChanged: (Date)->Any
#State private var selectedDay = Date()
#State private var currentPage = Page.withIndex(2)
#State private var data = Array(-2..<3)
#State private var disableButtons = false
var body: some View {
VStack(spacing: 0){
VStack(alignment: .center, spacing: 0){
Pager(page: currentPage,
data: data,
id: \.self) {
self.generateWeekView($0)
}
.singlePagination(ratio: 0.5, sensitivity: .high)
.onPageWillChange({ (page) in
withAnimation {
//Moving forward or backward a week
selectedDay = selectedDay + TimeInterval(604800) * Double((page - currentPage.index))
}
_ = OnDayChanged(selectedDay)
})
.onPageChanged({
page in
//Adding new weeks when we approach the boundaries of the array
if page == 1 {
let newData = (1...5).map { data.first! - $0 }.reversed()
withAnimation {
currentPage.update(.move(increment: newData.count))
data.insert(contentsOf: newData, at: 0)
}
} else if page == self.data.count - 2 {
guard let last = self.data.last else { return }
let newData = (1...5).map { last + $0 }
withAnimation {
data.append(contentsOf: newData)
}
}
disableButtons = false
})
.onDraggingBegan({
disableButtons = true
})
.pagingPriority(.simultaneous)
.frame(height: 48)
Capsule()
.frame(width: 32, height: 6)
.foregroundColor(Color("TransparetPurple"))
.padding(4)
}
.frame(maxWidth: .infinity)
.background(Color("AccentBlue"))
//Spacer()
}
}
The problem is when I adding element to the front of array Pager loses current page and goes to the next page on the left.
I tried to add one extra page to the index, but it didn't work
currentPage.update(.move(increment: newData.count + 1))
Maybe there is a more simple solution without providing an array? I want Pager to just give me the offset from first loaded page.
I looked into this and checked an example called InfiniteExampleView included in the git project for SwiftUIPager.
I noticed how they are able to add items to the array dynamically both in front of the current items and behind them.
I can't test your code completely since there are some missing parts to make it run the way you run it, but it seems when they insert in front of current items they update the page index as well and I don't see you do that in your code.
This is the code from the example:
if page == 1 {
let newData = (1...5).map { data1.first! - $0 }.reversed()
withAnimation {
page1.index += newData.count //<-- this line
data1.insert(contentsOf: newData, at: 0)
isPresented.toggle()
}
}
this is your code under the withAnimation part of if statement page==1:
currentPage.update(.move(increment: newData.count))
data.insert(contentsOf: newData, at: 0)
So maybe adding a line to change the index helps fix this. If not, please provide a more complete example to make it possible to debug it more easily.

Convert multiple detail tab views into single display

So I have an implementation on a project that someone else has made and I would like to switch the functionality of it, but I am unsure on how to do it, so I wanted to see if someone might be able to help me.
So I have the following code:
TabView(selection: $mainViewModel.selectedItemId) {
ForEach(mainViewModel.selectedCategory == "" ? mainViewModel.places: mainViewModel.places.filter({$0.category == mainViewModel.selectedCategory})) { item in
NavigationLink(
destination: PlaceDetailView(place: item),
label: {
PlaceTabViewDetail(item: item)
.background(Color("ColorWhite")).cornerRadius(20)
.frame(width: getRect().width - 40)
.padding(.horizontal, 10)
.padding(.bottom, 50)
.tag(item.id)
}
) //: END NAVIGATIONLINK
} //: END FOREACH
} //: END TABVIEW
Which outputs this:
How can I extract the "item" and use just a single display, so instead of being able to search through numerous locations tabs, I want just one single display like this:
Basically, I want to utilize "item" since it has all the data, but instead of a foreach, just have a single output.
If you want to show a single item from an array which is currently selected then you can try something like this.
Add a computed property in your MainViewModel.
var selectedItem: Place? {
places.first({ $0.id == selectedItemId })
}
Now use this property in your view like this.
if let item = mainViewModel.selectedItem {
NavigationLink(
destination: PlaceDetailView(place: item),
label: {
PlaceTabViewDetail(item: item)
.background(Color("ColorWhite")).cornerRadius(20)
.frame(width: getRect().width - 40)
.padding(.horizontal, 10)
.padding(.bottom, 50)
.tag(item.id)
}
)
}

SwiftUi what is the best way to number rows once you have a model

I have a view that gets data from an API what I want to do is rank rows based on points from highest to lowest (I am able to do that now) however issue is that I want to show row number like #1, #2 etc .. as you can see from below image in the red background it says 1 2 for both rows instead of the first saying 1 and the second row saying 2 . This is my code below . I have put a comment of // issue is here which is were the problem lies . As you can see I'm already using a forEach that passes in api data . Is there a way I can use that foreach ForEach(result) and number the rows from 1...n and still be able to retrieve the data like I'm doing . I have seen examples of people doing that but it is usually for a simple string array . Any suggestions would be great .
ScrollView(showsIndicators: false) {
LazyVStack {
let result = model.sorted {
$0.points! > $1.points!
}
ForEach(result) { value in
VStack(alignment: .leading) {
Spacer()
HStack {
ForEach(0..<result.count) { i in
Text("\(i + 1)")
.frame(width: 20, height: 20, alignment: .center)
.foregroundColor(.white)
.background(Color.red)
} // issue is here
Text(value.initials!)
.fontWeight(.bold)
.frame(width: 50, height: 50, alignment: .center)
.foregroundColor(Color(SystemColorPicked(picked: value.color)))
.padding()
.overlay(
Circle()
.stroke(Color.white, lineWidth: 4)
.padding(6)
)
VStack(alignment: .leading, spacing: 1) {
Text("\(value.fullname ?? "NA")")
.foregroundColor(.white)
.fontWeight(.bold)
Text("\(value.points ?? 0) | points")
.foregroundColor(.white)
}
}
}
}
}
}
The problem is you have a ForEach running within in item of your other ForEach. The second one is the one that is displaying "1 2" each time, since it iterates through all the results every time.
I don't have all of your code, so I'm not sure what type results is, but here's a sample that you can adapt to your use case. Keep in mind you'll be doing this to the outer (ie first) ForEach loop:
struct ContentView : View {
#State private var results = ["Test1","Test2","Test3"]
var body: some View {
VStack {
ForEach(Array(results.enumerated()), id: \.1) { (index, value) in
Text("\(index + 1) \(value)")
}
}
}
}
What's going on:
For the ForEach, I'm using results (or, result in your code) .enumerated -- this gives an index/item pair. I'm wrapping it in Array() because otherwise the ForEach won't want to take that specific type of sequence.
ForEach has to have an id -- here I'm just using the String itself. For your purpose, it might be different. .1 refers to the second item of the tuple (ie the item, not the index)
In the ForEach closure, you now get index and value

Breaking up the expression into distinct sub-expressions in a ForEach/ZStack (SwiftUI)

I have a little problem on the lastest SwiftUI, the error is "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
My code is like that :
let cards = ["Azertyuiop", "Bzertyuiop", "Czertyuiop", "Dzertyuiop", "Ezertyuiop", "Fzertyuiop", "Gzertyuiop", "Hzertyuiop", "Izertyuiop", "Jzertyuiop", "Kzertyuiop", "Lzertyuiop", "Bzertyuiop", "Czertyuiop", "Dzertyuiop", "Ezertyuiop", "Fzertyuiop", "Gzertyuiop", "Lzertyuiop", "Bzertyuiop", "Czertyuiop", "Dzertyuiop", "Ezertyuiop", "Fzertyuiop", "Gzertyuiop", "Lzertyuiop", "Bzertyuiop", "Czertyuiop", "Dzertyuiop", "Ezertyuiop", "Fzertyuiop", "Gzertyuiop"]
var body: some View {
ScrollView{
VStack (spacing: 0, content: {
ForEach(0..<cards.count/3) { row in // create number of rows
HStack (spacing: 0, content: {
ForEach(0..<3) { column in // create 3 columns
ZStack(alignment: .bottomLeading){
Image("ghost").resizable().aspectRatio(contentMode: .fill)
.overlay(Rectangle().fill (LinearGradient(gradient: Gradient(colors: [.clear, .black]),startPoint: .center, endPoint: .bottom)).clipped())
Text(self.cards[row * 3 + column]) // this cause the error
.fontWeight(.semibold)
}
}
})
}
})
}
}
I guess that the error comes from : row * 3 + column
So I tried to put the integer 1 instead of this calculation, and it worked.
How to do this calculation in my body and my View? because SwiftUI does not allow me and shows me "Expected pattern"
Thanks a lot !
There are two problems. One is using ZStack with overlay, the other is Linear Gradient is a view and you can use it directly.
ZStack(alignment: .bottomLeading){
Image("ghost").resizable().aspectRatio(contentMode: .fill)
LinearGradient(gradient: Gradient(colors: [.clear, .black]),startPoint: .center, endPoint: .bottom).clipped()
Text(self.cards[row * 3 + column]) // this cause the error
.fontWeight(.semibold)
}

SwiftUI fails to compile when attempting to filter data using string comparison

hoping someone can help me out. Been trying to figure out what's going on here with no luck. The app I am building contains the SwiftUI View listed below.
This View is embedded in another View which contains other List's, VStack's, etc. It is called when an item is selected to show another list of data based upon the user's selection.
It all looks, acts and works as intended (without data filtering).
For now, I am using a sample dataSet created using a simple Dictionary of data. When I attempt to apply a filter to this data by string comparison it causes a failure to compile with the following messages:
From Xcode:
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
From Canvas:
timedOutSpecific(30.0, operation: "a thunk to build")
In addition to these errors, the energy consumption of Xcode skyrockets until failure.
The code listed below will work if I remove the code self.dataSet == aRecord.module in the if statement and replace it with true. Any time I try to filter my dataset it results in these errors.
import SwiftUI
struct DataListView: View {
#State var titleBar = ""
#State private var showFavorites = false
#State private var showPriority = false
#State var dataSet = ""
var body: some View {
List{
ForEach (sampleData) { aRecord in
if (((aRecord.isFavorite && self.showFavorites) ||
(aRecord.isPriority && self.showPriority) ||
(!self.showPriority)) && self.dataSet == aRecord.module ){
VStack {
NavigationLink(destination: DetailView(titleBar: aRecord.title, statuteData: aRecord.statuteData, isFavorite: aRecord.isFavorite)) {
HStack {
Text(aRecord.module)
.font(.subheadline)
VStack(alignment: .leading) {
Text(aRecord.title)
}
.scaledToFit()
Spacer()
if aRecord.isFavorite {
Image(systemName: "star.fill")
.imageScale(.small)
.foregroundColor(.yellow)
}
}
}
}
.navigationBarTitle(self.titleBar)
.navigationBarItems(trailing:
HStack{
Button(action: {
self.showFavorites.toggle()
}) {
if self.showFavorites {
Image(systemName: "star.fill")
.imageScale(.large)
.foregroundColor(.yellow).padding()
} else {
Image(systemName: "star")
.imageScale(.large).padding()
}
}
Button(action: {
self.showPriority.toggle()
}) {
if self.showPriority {
Text("Priority")
} else {
Text("Standard")
}
}
})
}//endif
}
}//end foreach
}
}
struct TempCode_Previews: PreviewProvider {
static var previews: some View {
DataListView(dataSet: "myDataSetID")
}
}
The reason I believe that the string comparison is the culprit is, for one, it crashes as described above. I have also tried placing the conditional in other places throughout the code with the same results. Any time I apply this type of filter it causes this crash to occur.
Any advice is appreciated.
Thank you.
Break out that complex boolean logic into a function outside of the view builder that takes a record and returns a boolean & it should work.
I think the compiler struggles when there is complex logic inside of the body & can't verify return types etc etc.
Record Verification Function:
func verify(_ record: Record) -> Bool {
return (((record.isFavorite && showFavorites) ||
(record.isPriority && showPriority) ||
(!showPriority)) && dataSet == record.module )
}
Usage In Body:
if self.verify(aRecord) {