How to emulate the live lyrics animated gradient in swiftui? - swift

It's probably very complicated but would appreciate if someone could lead me in the right direction. I would like to create a flowing gradient animation like the one in the music app. I want to use it to change colours depending on lights for a smart home app. I am familiar with using LinearGradient and animating the start and end points, but this is obviously a lot more complicated but hopefully someone knows what I can do.
I found a MPUGradientView header that includes CAGradientLayer so I'm not sure if its combining gradients to somehow create the effect.
Thanks!

Not exactly reproducing the screenshot you were asking for, but it should get you started.
Inspired from here: https://nerdyak.tech/development/2019/09/30/animating-gradients-swiftui.html
struct MainView: View {
#State var gradient = [Color.red, Color.purple, Color.orange]
#State var startPoint = UnitPoint(x: 0, y: 0)
#State var endPoint = UnitPoint(x: 0, y: 2)
var body: some View {
ZStack {
LinearGradient(gradient: Gradient(colors: self.gradient), startPoint: self.startPoint, endPoint: self.endPoint)
.overlay(
Text("Hello world")
)
}
.edgesIgnoringSafeArea(.all)
.onAppear {
withAnimation(Animation.easeInOut(duration: 6).repeatForever(autoreverses: true)) {
self.startPoint = UnitPoint(x: 1, y: -1)
self.endPoint = UnitPoint(x: 0, y: 1)
}
}
}}

I have ended up getting something quite close in mostly SwiftUI but it requires an image.
Here is a preview of what it looks like:
https://twitter.com/priva2804/status/1284439692352434178?s=21
Here is the gist:
https://gist.github.com/Priva28/22fbae9dbe04a08fadf748793dd23d00

Related

SwiftUI Modifier Order

import SwiftUI
struct ContentView: View {
let letters = Array("Testing stuff")
#State private var enabled = false
#State private var dragAmount = CGSize.zero
var body: some View {
HStack(spacing: 0)
{
ForEach(0..<letters.count) { num in
Text(String(letters[num]))
.padding(5)
.font(.title)
.background(enabled ? .blue : .red)
.offset(dragAmount)
.clipShape(Circle())
.animation(.default.delay(Double(num) / 20), value: dragAmount)
}
}
.gesture(
DragGesture()
.onChanged { dragAmount = $0.translation}
.onEnded { _ in
dragAmount = .zero
enabled.toggle()
}
)
}
}
So I have this piece of code, I'm mostly just testing out animation stuff, originally the clipShape modifier was before the offset modifier and everything worked and went as expected, but when I move the clipShape modifier to be after the offset everything still animates but it seems like it's stuck in the Shape and I can't see anything outside the Circle.
Could someone maybe explain exactly why this is?
I understand the order of the modifiers is obviously very important in SwiftUI but to me it seems like whenever dragAmount changes then each view gets redrawn and offset and that offset modifier just notices changes in dragAmount and when it notices one it just offsets but why is it a problem if I put clipShape after that? Shouldn't it still totally be fine? I obviously don't understand why the order of the modifiers is important in this case so it would be nice if someone could clarify.
It doesn't work because the text is offsetted, and then clipShape clips the original location for the text.
Paul Hudson sums it up well:
Using offset() will cause a view to be moved relative to its natural position, but won’t affect the position of other views or any other modifiers placed after the offset.
- https://www.hackingwithswift.com/quick-start/swiftui/how-to-adjust-the-position-of-a-view-using-its-offset
Here, the only part you can see if the part of the yellow rectangle that is inside of the circle (outlined in red).
If you want the content to be clipped, then offsetted, use the clipShape modifier before the offset modifier.
Then it should look something like this (the part you can see is outlined in red):

SwiftUI What exactly happens when use .offset and .position Modifier simultaneously on a View, which decides the final location?

Here I have this question when I try to give a View an initial position, then user can use drag gesture to change the location of the View to anywhere. Although I already solved the issue by only using .position(x:y:) on a View, at the beginning I was thinking using .position(x:y:) to give initial position and .offset(offset:) to make the View move with gesture, simultaneously. Now, I really just want to know in more detail, what exactly happens when I use both of them the same time (the code below), so I can explain what happens in the View below.
What I cannot explain in the View below is that: when I simply drag gesture on the VStack box, it works as expected and the VStack moves with finger gesture, however, once the gesture ends and try to start a new drag gesture on the VStack, the VStack box goes back to the original position suddenly (like jumping to the original position when the code is loaded), then start moving with the gesture. Note that the gesture is moving as regular gesture, but the VStack already jumped to a different position so it starts moving from a different position. And this causes that the finger tip is no long on top of the VStack box, but off for some distance, although the VStack moves with the same trajectory as drag gesture does.
My question is: why the .position(x:y:) modifier seems only take effect at the very beginning of each new drag gesture detected, but during the drag gesture action on it seems .offset(offset:) dominates the main movement and the VStack stops at where it was dragged to. But once new drag gesture is on, the VStack jumps suddenly to the original position. I just could not wrap my head around how this behavior happens through timeline. Can somebody provide some insights?
Note that I already solved the issue to achieve what I need, right now it's just to understand what is exactly going on when .position(x:y:) and .offset(offset:) are used the same time, so please avoid some advice like. not use them simultaneously, thank you. The code bellow suppose to be runnable after copy and paste, if not pardon me for making mistake as I delete few lines to make it cleaner to reproduce the issue.
import SwiftUI
struct ContentView: View {
var body: some View {
ButtonsViewOffset()
}
}
struct ButtonsViewOffset: View {
let location: CGPoint = CGPoint(x: 50, y: 50)
#State private var offset = CGSize.zero
#State private var color = Color.purple
var dragGesture: some Gesture {
DragGesture()
.onChanged{ value in
self.offset = value.translation
print("offset onChange: \(offset)")
}
.onEnded{ _ in
if self.color == Color.purple{
self.color = Color.blue
}
else{
self.color = Color.purple
}
}
}
var body: some View {
VStack {
Text("Watch 3-1")
Text("x: \(self.location.x), y: \(self.location.y)")
}
.background(Color.gray)
.foregroundColor(self.color)
.offset(self.offset)
.position(x: self.location.x, y: self.location.y)
.gesture(dragGesture)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
}
}
}
Your issue has nothing to do with the use of position and offset. They actually both work simultaneously. Position sets the absolute position of the view, where as offset moves it relative to the absolute position. Therefore, you will notice that your view starts at position (50, 50) on the screen, and then you can drag it all around. Once you let go, it stops wherever it was. So far, so good. You then want to move it around again, and it pops back to the original position. The reason it does that is the way you set up location as a let constant. It needs to be state.
The problem stems from the fact that you are adding, without realizing it, the values of offset to position. When you finish your drag, offset retains the last values. However, when you start your next drag, those values start at (0,0) again, therefore the offset is reset to (0,0) and the view moves back to the original position. The key is that you need to use just the position or update the the offset in .onEnded. Don't use both. Here you have a set position, and are not saving the offset. How you handle it depends upon the purpose for which you are moving the view.
First, just use .position():
struct OffsetAndPositionView: View {
#State private var position = CGPoint(x: 50, y: 50)
#State private var color = Color.purple
var dragGesture: some Gesture {
DragGesture()
.onChanged{ value in
position = value.location
print("position onChange: \(position)")
}
.onEnded{ value in
if color == Color.purple{
color = Color.blue
}
else{
color = Color.purple
}
}
}
var body: some View {
Rectangle()
.fill(color)
.frame(width: 30, height: 30)
.position(position)
.gesture(dragGesture)
}
}
Second, just use .offset():
struct ButtonsViewOffset: View {
#State private var savedOffset = CGSize.zero
#State private var dragValue = CGSize.zero
#State private var color = Color.purple
var offset: CGSize {
savedOffset + dragValue
}
var dragGesture: some Gesture {
DragGesture()
.onChanged{ value in
dragValue = value.translation
print("dragValue onChange: \(dragValue)")
}
.onEnded{ value in
savedOffset = savedOffset + value.translation
dragValue = CGSize.zero
if color == Color.purple{
color = Color.blue
}
else{
color = Color.purple
}
}
}
var body: some View {
Rectangle()
.fill(color)
.frame(width: 30, height: 30)
.offset(offset)
.gesture(dragGesture)
}
}
// Convenience operator overload
func + (lhs: CGSize, rhs: CGSize) -> CGSize {
return CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height)
}

How to use user input in function in swiftui?

I have a View, in which the user is able to enter some text into a TextField. I want to be able to get the text, which was entered in the TextField and use this value inside of a struct. The concept of the app, is that it shows the elevation degree of the sun. To be able to do this, it is scraping the values from a WebPage. However to make this app dynamic, the user has to be able to edit the url (you can change location, date etc in the url). I thought this would be fairly easy, since I only have to get some text, and edit a url before the url is being loaded. I have been able to pass the value into a view, however I need it in a struct. Maybe the whole "layout of my code is wrong, maybe I should get the data and draw the function in a view? I don't know. This is my first time coding with swift.
I want to change the latitude var.
This is my code:
View 1 (Input):
class ViewModel: ObservableObject {
#Published var latitude:String = ""
#Published var page = 0
}
struct ContentView: View {
#EnvironmentObject var value1: ViewModel
var body: some View {
if value1.page == 0{
VStack{
TextField("", text: $value1.latitude)
Button(action:{ value1.page = 1}){
Text("To next view")
}.frame(width: 300, height: 100, alignment: .center)
}
} else {
elevationGraph()
}
}
}
View 2 (Graph)
struct getHtml {
var url = URL(string: "https://midcdmz.nrel.gov/apps/spa.pl?syear=2020&smonth=1&sday=1&eyear=2020&emonth=1&eday=1&otype=0&step=60&stepunit=1&hr=12&min=0&sec=0&latitude=\(latitude)&longitude=10.757933&timezone=1.0&elev=53&press=835&temp=10&dut1=0.0&deltat=64.797&azmrot=180&slope=0&refract=0.5667&field=0")
func loadData(from url: URL?) -> String {
guard let url = url else {
return "nil"
}
let html = try! String(contentsOf: url, encoding: String.Encoding.utf8)
return html
}
}
struct elevationFunction: Shape {
var url: URL? //This only works in views, is there a way to do it in shape structs?
let html = getHtml.init().loadData(from: getHtml.init().url)
private func dbl1() -> Double {
let leftSideOfTheValue = "0:00:00,"
let rightSideOfTheValue = "\(month)/\(day)/\(year),1:00:00,"
guard let leftRange = html.range(of: leftSideOfTheValue) else {
print("cant find left range")
return 0
}
guard let rightRange = html.range(of: rightSideOfTheValue) else {
print("cant find right range")
return 0
}
let rangeOfTheValue = leftRange.upperBound..<rightRange.lowerBound
return Double(html[rangeOfTheValue].dropLast()) ?? 90
}
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 10, y: (125 - (90-dbl1()))))
path.addLine(to: CGPoint(x: 120, y: (125 - (90-45))))
path.addLine(to: CGPoint(x: 250, y: (125 - (90-dbl1()))))
var scale = (rect.height / 350) * (9/10)
var xOffset = (rect.width / 6)
var yOffset = (rect.height / 2)
return path.applying(CGAffineTransform(scaleX: scale, y: scale)).applying(CGAffineTransform(translationX: xOffset, y: yOffset))
}
}
struct elevationGraph: View {
var body: some View {
GeometryReader { geometry in
ZStack {
elevationFunction().stroke(LinearGradient(gradient: Gradient(colors: [Color.yellow, Color.red]), startPoint: .top , endPoint: .bottom), style: StrokeStyle(lineWidth: 6.0)).aspectRatio(contentMode: .fill)
}
.frame(width: 600, height: 800, alignment: .center)
}
}
}
As mentioned in my comment, you can pass a parameter to a Shape just like you can a regular View:
elevationFunction(url: yourURL)
Best practice would be to capitalize this and name it ...Shape as well:
elevationFunction becomes ElevationShape
Regarding your second question in the comment, first, you may want to fix the naming of getHtml for the same reason as above -- uncapitalized, it looks like a variable name. Maybe something like DataLoader.
Regarding the crash, you have some circular logic going on -- you call getHtml.init() and then pass a parameter that is again derived from getHtml.init() again. Why not just call getHtml() and have it loadData from its own internal URL property?
There's a larger problem at work, though, which is that you've declared html as a let property on your Shape, which is going to get recreated every time your Shape is rendered. So, on every render, with your current code, you'll create 2 new getHtmls and attempt to load the data (which very well may not actually have time to load the URL request). This very well could be blocking the first render of the Shape as well and is almost certainly causing your crash somewhere in the circular and repetitive logic going on.
Instead, you might want to consider moving your URL request to onAppear or as part of an ObservableObject where you can have a little more control of when and how often this data gets loaded. Here's a good resource on learning more about loading data using URLSession and SwiftUI: https://www.hackingwithswift.com/books/ios-swiftui/sending-and-receiving-codable-data-with-urlsession-and-swiftui

ScrollView or List, How to make a particular scrollable view (SwiftUI)

So I am trying to make this List or ScrollView in SwiftUI (not fully iOS 14 ready yet, I have to use the old api, so I can't use ScrollViewReader or other fantasy from iOS 14+ SwiftUI api).
The goal is to have only a specific number of rows visible on the view and be able to center the last one.
Images always make it easier to explain. So it should look somewhat like this.
the first two rows have a specific color applied to them, the middle one also but has to be center vertically. then there are eventually 2 more rows under but they are invisible at the moment and will be visible by scrolling down and make them appear.
The closest example i have of this, is the Apple Music Lyrics UI/UX if you are familiar with it.
I am not sure how to approach this here. I thought about create a List that will have a Text with a frame height defined by the height of the view divided by 5. But then I am not sure how to define a specific color for each row depending of which one is it in the view at the moment.
Also, It would be preferable if I can just center the selected row and let the other row have their own sizes without setting one arbitrary.
Banging my head on the walls atm, any help is welcomed! thank you guys.
Edit 1:
Screenshot added as a result from #nicksarno answer. Definitely looking good. The only noted issue atm is that it highlights multiple row with the same color instead of one.
Edit 2:
I did some adjustment to the code #nicksarno published.
let middleScreenPosition = geometryProxy.size.height / 2
return ScrollView {
VStack(alignment: .leading, spacing: 20) {
Spacer()
.frame(height: geometryProxy.size.height * 0.4)
ForEach(playerViewModel.flowReaderFragments, id: \.id) { text in
Text(text.content) // Outside of geometry ready to set the natural size
.opacity(0)
.overlay(
GeometryReader { geo in
let midY = geo.frame(in: .global).midY
Text(text.content) // Actual text
.font(.headline)
.foregroundColor( // Text color
midY > (middleScreenPosition - geo.size.height / 2) && midY < (middleScreenPosition + geo.size.height / 2) ? .white :
midY < (middleScreenPosition - geo.size.height / 2) ? .gray :
.gray
)
.colorMultiply( // Animates better than .foregroundColor animation
midY > (middleScreenPosition - geo.size.height/2) && midY < (middleScreenPosition + geo.size.height/2) ? .white :
midY < (middleScreenPosition - geo.size.height/2) ? .gray :
.clear
)
.animation(.easeInOut)
}
)
}
Spacer()
.frame(height: geometryProxy.size.height * 0.4)
}
.frame(maxWidth: .infinity)
.padding()
}
.background(
Color.black
.edgesIgnoringSafeArea(.all)
)
Now it is closer to what I am looking for. Also, I added some Spacer (not dynamic yet) that will allow to center the text if needed in the middle. Still need to figure how to put the text in the middle when selected one of the element.
Here's how I would do it. First, add a GeometryReader to get the size of the entire screen (screenGeometry). Using that, you can set up the boundaries for your text modifiers (upper/lower). Finally, add a GeoemteryReader behind each of the Text() and (here's the magic) you can use the .frame(in: global) function to find its current location in the global coordinate space. We can then compare the frame coordinates to our upper/lower boundaries.
A couple notes:
The current boundaries are at 40% and 60% of the screen.
I'm currently using the .midY coordinate of the Text geometry, but you can also use .minY or .maxY to get more exact.
The natural behavior of a GeometryReader sizes to the smallest size to fit, so to keep the natural Text sizes, I added a first Text() with 0% opacity (for the frame) and then added the GeometryReader + the actual Text() as an overlay.
Lastly, the .foregroundColor doesn't really animate, so I also added a .colorMultiply on top, which does animate.
Code:
struct ScrollViewPlayground: View {
let texts: [String] = [
"So I am trying to make this List or ScrollView in SwiftUI (not fully iOS 14 ready yet, I have to use the old api, so I can't use ScrollViewReader or other fantasy from iOS 14+ SwiftUI api). The goal is to have only a specific number of rows visible on the view and be able to center the last one.",
"Images always make it easier to explain. So it should look somewhat like this.",
"the first two rows have a specific color applied to them, the middle one also but has to be center vertically. then there are eventually 2 more rows under but they are invisible at the moment and will be visible by scrolling down and make them appear.",
"The closest example i have of this, is the Apple Music Lyrics UI/UX if you are familiar with it.",
"I am not sure how to approach this here. I thought about create a List that will have a Text with a frame height defined by the height of the view divided by 5. But then I am not sure how to define a specific color for each row depending of which one is it in the view at the moment.",
"Also, It would be preferable if I can just center the selected row and let the other row have their own sizes without setting one arbitrary.",
"Banging my head on the walls atm, any help is welcomed! thank you guys."
]
var body: some View {
GeometryReader { screenGeometry in
let lowerBoundary = screenGeometry.size.height * 0.4
let upperBoundary = screenGeometry.size.height * (1.0 - 0.4)
ScrollView {
VStack(alignment: .leading, spacing: 20) {
ForEach(texts, id: \.self) { text in
Text(text) // Outside of geometry ready to set the natural size
.opacity(0)
.overlay(
GeometryReader { geo in
let midY = geo.frame(in: .global).midY
Text(text) // Actual text
.font(.headline)
.foregroundColor( // Text color
midY > lowerBoundary && midY < upperBoundary ? .white :
midY < lowerBoundary ? .gray :
.gray
)
.colorMultiply( // Animates better than .foregroundColor animation
midY > lowerBoundary && midY < upperBoundary ? .white :
midY < lowerBoundary ? .gray :
.clear
)
.animation(.easeInOut)
}
)
}
}
.frame(maxWidth: .infinity)
.padding()
}
.background(
Color.black
.edgesIgnoringSafeArea(.all)
)
}
}
}
struct ScrollViewPlayground_Previews: PreviewProvider {
static var previews: some View {
ScrollViewPlayground()
}
}

How to get .onHover(...) events for custom Shapes

Say you have a Circle. How can you change its color when you hover inside the circle?
I tried
struct ContentView: View {
#State var hovered = false
var body: some View {
Circle()
.foregroundColor(hovered ? .purple : .blue)
.onHover { self.hovered = $0 }
}
}
But this causes hovered to be true even when the mouse is outside of the circle (but still inside its bounding box).
I noticed that the .onTapGesture(...) uses the hit testing of the actual shape and not the hit testing of its bounding box.
So how can we have similar hit testing behavior as the tap gesture but for hovering?
The answer depends on the precision you need. The hove in SwiftUI currently just monitor the MouseEnter and MouseExit events, so the working region for a view is the frame which is a Rectangle.
You may build a backgroundView in ZStack which can compose those Rectangle with a customized algorithm. In circle shape, it should be like a matrix with different some small rectangles. One oversimplified example could be like the following.
ZStack{
VStack{
HStack{
Rectangle().frame( width:100, height: 100).offset(x: -50, y: 0)
Rectangle().frame( width:100, height: 100).offset(x: 50, y: 0)
}}.onHover{print($0)}
Rectangle().foregroundColor(hovered ? .purple : .blue).clipShape(Circle())
}