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
Related
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.
I am making an earthquake application on SwiftUI. The data I receive includes the magnitude of the earthquake and I want the text I print on the screen to change color according to this magnitude.
For example, I can say: I want it to show green if it is less than 3, yellow if it is between 3 and 5, red if it is between 5 and 7, black if it is greater than 7.
Can you help me?
Thanks
There are many ways to solve this problem, I believe if you know how to do an if-else statement, you can do it yourself. Since you're struggling with probably swift or swiftUI itself
here are 3 ways ways you can do it
first is
struct ContentView: View {
#State var quake = 2
var body: some View {
if (quake < 3) {
Text("Earthquake")
.foregroundColor(.green)
} else if (quake <= 5) {
Text("Earthquake")
.foregroundColor(.yellow)
} else if // etc...
}
}
but it's very inefficient, therefore the 2nd solution is to iterate it within the foreground color itself
struct ContentView: View {
#State var quake = 2
var body: some View {
Text("Earthquake")
.foregroundColor(quake < 3 ? .green : quake <= 5 ? .yellow : // etc)
}
}
all you have to do is put the
condiditon ? if its true : if codition ? if it's true : if condition //etc untill else
it's basicaly like
if condition then do something
else if condition
up untill
else
the last solution is to create a function that returns the key, i believe this one is far more beginner-friendly compared to the 2nd one
struct ContentView: View {
#State var quake = 2
private func getColor() -> Color {
if quake <= 3 {
return .green
} else if quake <= 5 {
return .yellow
}
return .black // default, random, you decide the last condition
}
var body: some View {
Text("Earthquake")
.foregroundColor(self.getColor())
}
}
You can use ternary operators to show green if it is less than 3, yellow if it is between 3 and 5, red if it is between 5 and 7, black if it is greater than 7. For example:
struct ContentView: View {
#State var quake = 2
var body: some View {
Text("quake in πΊπ¦")
.foregroundColor(quake < 3 ? .green
: quake <= 5 ? .yellow
: quake <= 7 ? .red
: .black)
}
}
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.
When I have multiple text fields rendered in SwiftUI in a given view, I am getting noticeable lag that is directly proportional to the number of text fields. If I change these to simple text views, the lag goes down considerably.
I have looked at SO and found a few questions about lag with TextField but generally it seems like there's a preponderance that the lag is caused by the data source because when using a constant value, the lag is not observed.
I have created a demo project to illustrate the issue. I have an array of 20 contact names and for each name create a contact card with three email addresses. If I toggle the view between rendering the email addresses as Text vs TextField Views (with a constant value), the time taken from button tap to the last view's .onAppear is 80-100 ms (Text) and 300-320 ms (TextField).
Both views take a noticeable time to render, but clearly the TextFields take a significantly longer time to render on this contrived, trivial app. In our app, we are rendering significantly more information and not using constant values for the TextFields so this lag produces more pronounced effects (sometimes a few seconds). Is there some way around this issue for SwiftUI TextFields? Below is the code for the demo project. I know there are better ways to write the code, just threw it together quickly to demonstrate the speed issues.
Also, interestingly, if I put the ForEach into a List (or just try to use a list directly from the array data), no ContactCard views are rendered at all.
Any help is greatly appreciated!
import SwiftUI
var formatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
return formatter
}
struct ContentView: View {
let contacts: Array<(first: String, last: String)> = [
("John", "Stone"),
("Ponnappa", "Priya"),
("Mia", "Wong"),
("Peter", "Stanbridge"),
("Natalie", "Lee-Walsh"),
("Ang", "Li"),
("Nguta", "Ithya"),
("Tamzyn", "French"),
("Salome", "Simoes"),
("Trevor", "Virtue"),
("Tarryn", "Campbell-Gillies"),
("Eugenia", "Anders"),
("Andrew", "Kazantzis"),
("Verona", "Blair"),
("Jane", "Meldrum"),
(" Maureen", "M. Smith"),
("Desiree", "Burch"),
("Daly", "Harry"),
("Hayman", "Andrews"),
("Ruveni", "Ellawala")
]
#State var isTextField = false
var body: some View {
ScrollView {
VStack {
HStack {
Button("Text") {
print("text tapped: \(formatter.string(from: Date()))")
isTextField = false
}
Button("TextField") {
print("text tapped: \(formatter.string(from: Date()))")
isTextField = true
}
}
ForEach(contacts, id: \.self.last) { contact in
ContactCard(name: contact, isTextField: $isTextField)
}
}
}
}
}
struct ContactCard: View {
var name: (first: String, last: String)
#Binding var isTextField: Bool
var emailAddresses: Array<String> {
[
"\(name.first).\(name.last)#home.com",
"\(name.first).\(name.last)#work.com",
"\(name.first).\(name.last)#work.org",
]
}
var body: some View {
VStack {
Text("\(name.first) \(name.last)")
.font(.headline)
ForEach(emailAddresses, id: \.self) { email in
HStack {
Text("Email")
.frame(width: 100)
if isTextField {
TextField("", text: .constant(email))
.onAppear(){
print("view appeared: \(formatter.string(from: Date()))")
}
} else {
Text(email)
.onAppear(){
print("view appeared: \(formatter.string(from: Date()))")
}
}
Spacer()
}
.font(.body)
}
}
.padding()
}
}
Use LazyVStack in your scroll view instead of VStack. It worked for me, tested using 200 contact names.
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?