How to implement infinite swipable week view in SwiftUI? - swift

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.

Related

is there a way to change Text values automatically with Animation?

I have a loading screen where I want to show a text changing its value automatically with an animation.
I have my logo rotating indefinitely without any button action
//logo
Image("reny")
.rotationEffect(.degrees(rotateDegree))
.onAppear(perform: {
withAnimation(Animation.linear(duration: 4).repeatForever(autoreverses: false)) {
self.rotateDegree = 360
}
})
I assumed it was possible to do the same for a text using a string array but it doesn't work
#State var texts = ["Find the Apartment you like", "send an application", "we'll approve you in secs baby!"]
#State var textIndex : Int = 0
//introduction text
Text(texts[textIndex]).bold()
.font(.title)
.onAppear(perform: {
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
textIndex += 1
}
})
does anybody know how to change the value of a text with an animation automatically?
my intention is to show how to use the app during this loading time.
Here is a possible approach - just to replace Text depending on index and continuously change index after appear.
Tested with Xcode 13.4 / iOS 15.5
Main part:
private func next() {
var next = textIndex + 1
if next == texts.count {
next = 0
}
withAnimation(Animation.linear(duration: 2)) {
textIndex = next
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.next()
}
}
and text itself
Text(texts[textIndex]).bold()
.font(.title).id(textIndex)
.onAppear(perform: {
next()
})
Test module on GitHub

SwiftUI ForEach with index - "The compiler is unable to type-check this expression in reasonable time"

In a swiftUI view that I'm writing, I need to use a ForEach, accessing each element of a list and its index. Most of the information I could find about this said to use .enumerated() as in ForEach(Array(values.enumerated()), id: \.offset) { index, value in }
However when I try to do that in my view:
/// A popover displaing a list of items.
struct ListPopover: View {
// MARK: Properties
/// The array of vales to display.
var values: [String]
/// Whether there are more values than the limit and they are concatenated.
var valuesConcatenated: Bool = false
/// A closure that is called when the button next to a row is pressed.
var action: ((_ index: Int) -> Void)?
/// The SF symbol on the button in each row.
var actionSymbolName: String?
// MARK: Initializers
init(values: [String], limit: Int = 10) {
if values.count > limit {
self.values = values.suffix(limit - 1) + ["\(values.count - (limit - 1)) more..."]
valuesConcatenated = true
} else {
self.values = values
}
}
// MARK: Body
var body: some View {
VStack {
ForEach(Array(values.enumerated()), id: \.offset) { index, value in
HStack {
if !(index == values.indices.last && valuesConcatenated) {
Text("\(index).")
.foregroundColor(.secondary)
}
Text(value)
Spacer()
if action != nil && !(index == values.indices.last && valuesConcatenated) {
Spacer()
Button {
action!(index)
} label: {
Image(systemName: actionSymbolName ?? "questionmark")
}
.frame(alignment: .trailing)
}
}
.if((values.count - index) % 2 == 0) { view in
view.background(
Color(.systemGray5)
.cornerRadius(5)
)
}
}
}
}
}
I get the error The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions on the line var body: some View {
I've also noticed that this code causes some other problems like making the Xcode autocomplete extremely slow.
Any ideas how I might be able to solve this? It seems like a pretty simple view and I think I'm doing the ForEach how I should.
Thanks!
This is a very misleading error. What it really means is you screwed something up in your body, but the compiler can't figure out the error, so it throws it on the body itself. The easiest way to find it is to comment out portions of your body in matched braces until the error goes away. In your case the issue is with this:
.if((values.count - index) % 2 == 0) { view in
view.background(
Color(.systemGray5)
.cornerRadius(5)
)
}
I am not sure what you are attempting to do, but .if is not valid syntax and I am not sure what view is or where it is supposed to come from.

How to build NumberField in SwiftUI?

I'm working on MacOS and I try to make NumberField — something like TextField, but for numbers. In rather big tree of views at the top I had:
...
VStack {
ForEach(instances.indices, id:\.self) {index in
TextField("",
text: Binding(
get: {"\(String(format: "%.1f", instances[index].values[valueIndex]))"},
set: {setValueForInstance(index, valueIndex, $0)})
)
}
}
...
And it worked well, but not nice:
✔︎ when I changed value, all View structure was redrawn – good
✔︎ values was updated if they were changed by another part of Views structure – good
✖︎ it was updated after each keypresses, which was annoying, when I tried to input 1.2, just after pressing 1 view was updated to 1.0. Possible to input every number but inconvenient – bad
So, I tried to build NumberField.
var format = "%.1f"
struct NumberField : View {
#Binding var number: Double {
didSet {
stringNumber = String(format: format, number)
}
}
#State var stringNumber: String
var body: some View {
TextField("" , text: $stringNumber, onCommit: {
print ("Commiting")
if let v = Double(stringNumber) {
number = v
} else {
stringNumber = String(format:format, number)
}
print ("\(stringNumber) , \(number)" )
})
}
init (number: Binding<Double>) {
self._number = number
self._stringNumber = State(wrappedValue: String(format:format, number.wrappedValue))
}
}
And It's called from the same place as before:
...
VStack {
ForEach(instances.indices, id:\.self) {index in
NumberField($instances[index].values[valueIndex])
}
}
...
But in this case it never updates NumberField View if values was changed by another part of View. Whats's wrong? Where is a trick?

DispatchQueue triggering foreach loop twice with SwiftUI

I'm having a weird behavior with my code.
There is View with text that is getting its text values from the User object.
If I take the reference out and leave it with just String everything is good, but with it the foreach loop that is getting this value runs twice.
Also, if I put the assignment out from DispatchQueue it runs once but the view isn't updating itself (because its a #Published var).
Even if I remove #Published it still run twice.
I'm sure there is a simple explanation for this behavior but I really don't know what could make this happen.
Better explanation in code with comments:
The view:
struct MenuGroupBoxList: View {
#EnvironmentObject var session: SessionStore
var columns = [
GridItem(.adaptive(minimum: 100, maximum: 200), spacing: 8),
GridItem(.adaptive(minimum: 100, maximum: 200), spacing: 8)
]
var body: some View {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(MenuTitles.allCases, id:\.self) { item in
if item == .sentShifts{
GroupColumn(menuItem: item, unit: Text(session.user?.totalShiftConsts.description ?? "0"))
}else if item == .weekdays{
//Deleting this line: session.user?.totalShifts.description. will run the loop once.
GroupColumn(menuItem: item, unit: Text(session.user?.totalShifts.description ?? "0"))
}else{
GroupColumn(menuItem: item, unit: Text("0"))
}
}
}.background(Color(UIColor.systemGroupedBackground))
}
}
Where the foreach loop:
enum ChooseWeekdays{
case current, next
}
class WeekdaysModel: Identifiable, ObservableObject{
private var session = SessionStore.shared
private var date = Date()
#Published var week = [Day]()
#Published var isFetching = false
init(_ weekdays: ChooseWeekdays){
switch weekdays {
case .current:
currentWeekdaysFetch()
case .next:
nextWeekdaysFetch()
}
}
private func currentWeekdaysFetch(){
DispatchQueue.main.async {
self.isFetching = true
var count = 0
for (index, symbol) in self.date.weekDaySymbols.enumerated(){
let day = Day(date: self.date.currentWeek[index], isSelected: false, hasShifts: false, daySymbol: symbol, dayNumber: self.date.currentWeekNumbers[index])
day.setShifts(morning: self.session.employeesList, middle: self.session.employeesList, evening: self.session.employeesList)
if index == Date().getCurrentDayIndex(){
day.isSelected = true
}
count = count + day.shifts.compactMap({$0.employees.contains(where: {$0.uid == self.session.user?.uid})}).count
self.week.append(day)
self.isFetching = false
//Printing to see if loop run twice.
print("Index:", index)
}
//OR - take this line out from DispatchQueue, will run once but wouldn't update the UI because totalShifts is a #Published var
self.session.user?.totalShifts = count
}
}
}
The results I'm getting in print is Index: 0...6 twice.
Where WeekdaysModel created:
struct WeekdaysView: View {
#ObservedObject var viewModel = WeekdaysModel(.current)
var body: some View {
ScrollView{
WeekRow(model: viewModel)
ForEach(viewModel.week){ day in
ShiftTypesView(day: day)
}
}
.navigationTitle(LocalizedStringKey("Weekdays"))
.background(Color(UIColor.systemGroupedBackground))
.edgesIgnoringSafeArea(.bottom)
}
}
There is another initiation in a different file but different enum (WeekdaysModel(.next))
seems a common case on SwiftUI.
I did experiment similar behaviour filling cells of a tableview being called twice for every cell: it seems swiftUI fills every viewed elements twice (I assume to satisfy rotation)
As suggested fill a data structure ONCE out of swiftUI "building of views"
(your data will be read anyway twice, but no double expensive calls to DB or network)
This behaviour is very different from std UiKit and can be frustrating if network is involved (calls will be issued twice... double network completions, errors on servers (for example double login and so on...)

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) {