SwiftUI onTapGesture interact with caller only - swift

I've just started looking into SwiftUI and since it's so different I'm trying to wrap my head around basic concepts.
In this scenario, how would I go about changing the color only for the circle tapped?
ForEach(1...count, id: \.self) { _ in
Circle()
.foregroundColor(colors.randomElement()).opacity(0.2)
.frame(width: .random(in: 10...100), height: .random(in: 10...100))
.position(x: .random(in: self.stepperValue...400),
y: .random(in: self.stepperValue...400))
.saturation(2.0)
.onTapGesture(count: 1, perform: {
// Change color of circle that was tapped
print("Tapped")
})
.animation(.default) // Animate the change in position
}

You create a View that has the "Rows" individual properties
import SwiftUI
struct SampleColorChangeView: View {
//If the options are fixed no need to keep an eye on them
//You can move this down to the Row if you don't need to have them available here
let colors: [Color] = [.red,.blue,.gray, .yellow,.orange]
#State var count: Int = 10
var body: some View {
VStack{
ForEach(1...count, id: \.self) { _ in
RowView(colors: colors)
}
}
}
}
//Create a row View to observe individual objects
//You will do this with anything that you want to Observe independently
struct RowView: View {
let colors: [Color]
//#State observes changes so the View is updated
#State var color: Color = .blue
//This kind of works like the colors do you want one for each or a shared for all. Does the parent need access? You can move it up or keep it here
#State var stepperValue: CGFloat = 0
//The only change here is the reference to the individual Color
var body: some View {
Circle()
//You set the individual color here
.foregroundColor(color).opacity(0.2)
.frame(width: .random(in: 10...100), height: .random(in: 10...100))
.position(x: .random(in: self.stepperValue...400),
y: .random(in: self.stepperValue...400))
.saturation(2.0)
.onTapGesture(count: 1, perform: {
// Change color of circle that was tapped
color = colors.randomElement()!
print("Tapped")
})
.animation(.default) // Animate the change in position
//If you want to set a random color to start vs just having them all be the same Color you can do something like this
.onAppear(){
color = colors.randomElement()!
}
}
}
struct SampleColorChangeView_Previews: PreviewProvider {
static var previews: some View {
SampleColorChangeView()
}
}

Well there are two main options I see here
Make a custom view like
struct MyCircle: View {
#State var color: Color?
var body: some View {
Circle()
.foregroundColor(color)
.onTapGesture {
self.color = colors.randomElement()
}
}
}
and then integrate that or
Use a model for your color
struct MyView: View {
#State var colors = allColors.indices.compactMap { _ in allColors.randomElement() }
var body: some View {
ForEach(colors.indices) { index in
Circle()
.foregroundColor(colors[index])
.onTapGesture {
colors[index] = allColors.randomElement()
}
}
}
}
A state like this should preferably be in its own class which should be inserted as ObservedObject.

Related

SwiftUI and CoreData: Horizontal bar chart with different coloured values

I am learning SwiftUI with CoreData and I have that one demo app where I am stuck.
Outcome:
I would like to present different coloured bar charts based on numeral values (calories). I managed to present numeral values with one colour, but not different colours based on value.
Here is image about desired outcome and my Swift code.
Thank you for any help! -Toni
Desired UI outcome
Code screenshot
import SwiftUI
struct ColourChart: View {
#Environment(\.managedObjectContext) var managedObjContext
#State private var name = ""
#State private var calories: Double = 0
#State private var color: Color = .gray
#FetchRequest(sortDescriptors: [SortDescriptor(\.calories)]) var food: FetchedResults<CaloriesEntity>
var body: some View {
List {
ForEach(food) { food in
VStack(alignment: .leading) {
HStack(alignment: .top) {
Text(food.name ?? "Unknown name")
.onAppear {
calories = food.calories
name = food.name!
}
Spacer()
Text(calcTimeSince(date:food.date!))
.foregroundColor(.gray)
.font(.caption)
.italic()
}///-HStack
ZStack {
Rectangle()
.foregroundColor(ColouredBars())
.frame(width: food.calories/3, height: 15, alignment: .trailing)
.cornerRadius(5)
Text("\(Int(food.calories))")
.foregroundColor(.white)
.font(.caption)
}///-ZStack
}///-VStack
}///ForEach Ends
}///-List
}///-View
func ColouredBars() -> Color {
let calories: Double = 299
if calories > 600 {
color = .red
} else if calories > 300 {
color = .yellow
} else {
color = .green
}
return color
}
}///-Struct
struct ColourChart_Previews: PreviewProvider {
static var previews: some View {
ColourChart()
}
}
You would want to add a calories parameter and change the return type to color. The code would look something like this:
func calorieColor(calorie: Double) -> Color {
if calorie >= 600 {
return .red
} else if calorie >= 300 {
return .yellow
} else {
return .green
}
return .clear
}
the .clear is there just to allow the function to return some type of color if the if statement isn't applicable but that shouldn't be the case.
You could also do func calorieColor(_ calorie: Double) -> Color { The _ allows you to make the code a little more concise by allowing you to just type calorieColor(150) vs calorieColor(Calorie: 150).

Creating a drag handle inside of a SwiftUI view (for draggable windows and such)

I'm trying to learn SwiftUI, and I had a question about how to make a component that has a handle on it which you can use to drag around. There are several tutorials online about how to make a draggable component, but none of them exactly answer the question I have, so I thought I would seek the wisdom of you fine people.
Lets say you have a view that's like a window with a title bar. For simplicity's sake, lets make it like this:
struct WindowView: View {
var body: some View {
VStack(spacing:0) {
Color.red
.frame(height:25)
Color.blue
}
}
}
I.e. the red part at the top is the title bar, and the main body of the component is the blue area. Now, this window view is contained inside another view, and you can drag it around. The way I've read it, you should do something like this (very simplified):
struct ContainerView: View {
#State private var loc = CGPoint(x:150, y:150);
var body: some View {
ZStack {
WindowView()
.frame(width:100, height:100)
.position(loc)
.gesture(DragGesture()
.onChanged { value in
loc = value.location
}
)
}
}
}
and that indeed lets you drag the component around (ignore for now that we're always dragging by the center of the image, it's not really the point):
However, this is not what I want: I don't want you to be able to drag the component around by just dragging inside the window, I only want to drag it around by dragging the red title bar. But the red title-bar is hidden somewhere inside of WindowView. I don't want to move the #State variable containing the position to inside the WindowView, it seems to me much more logical to have that inside ContainerView. But then I need to somehow forward the gesture into the embedded title bar.
I imagine the best way would be for the ContainerView to look something like this:
struct ContainerView: View {
#State private var loc = CGPoint(x:150, y:150);
var body: some View {
ZStack {
WindowView()
.frame(width:100, height:100)
.position(loc)
.titleBarGesture(DragGesture()
.onChanged { value in
loc = value.location
}
)
}
}
}
but I don't know how you would implement that .titleBarGesture in the correct way (or if this is even the proper way to do it. should the gesture be an argument to the WindowView constructor?). Can anyone help me out, give me some pointers?
Thanks in advance!
You can get smooth translation of the window using the offset from the drag, and then disable touch on the background element to prevent content from dragging.
Buttons still work in the content area.
import SwiftUI
struct WindowBar: View {
#Binding var location: CGPoint
var body: some View {
ZStack {
Color.red
.frame(height:25)
Text(String(format: "%.1f: %.1f", location.x, location.y))
}
}
}
struct WindowContent: View {
var body: some View {
ZStack {
Color.blue
.allowsHitTesting(false) // background prevents interaction
Button("Press Me") {
print("Tap")
}
}
}
}
struct WindowView: View, Identifiable {
#State var location: CGPoint // The views current center position
let id = UUID()
/// Keep track of total translation so that we don't jump on finger drag
/// SwiftUI doesn't have an onBegin callback like UIKit's gestures
#State private var startDragLocation = CGPoint.zero
#State private var isBeginDrag = true
init(location: CGPoint = .zero) {
_location = .init(initialValue: location)
}
var body: some View {
VStack(spacing:0) {
WindowBar(location: $location)
WindowContent()
}
.frame(width: 100, height: 100)
.position(location)
.gesture(DragGesture()
.onChanged({ value in
if isBeginDrag {
isBeginDrag = false
startDragLocation = location
}
// In UIKit we can reset translation to zero, but we can't in SwiftUI
// So we do book keeping to track startLocation of gesture and adjust by
// total translation
location = CGPoint(x: startDragLocation.x + value.translation.width,
y: startDragLocation.y + value.translation.height)
})
.onEnded({ value in
isBeginDrag = true /// reset for next drag
}))
}
}
struct ContainerView: View {
#State private var windows = [
WindowView(location: CGPoint(x: 50, y: 100)),
WindowView(location: CGPoint(x: 190, y: 75)),
WindowView(location: CGPoint(x: 250, y: 50))
]
var body: some View {
ZStack {
ForEach(windows) { window in
window
}
}
.frame(width: 600, height: 480)
}
}
struct ContentView: View {
var body: some View {
ContainerView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can just use .allowsHitTesting(false) on the blue view, which will ignore the touch gesture on that view. Hence, you can only drag on the red View and still have the DragGesture outside that view.
struct WindowView: View {
var body: some View {
VStack(spacing:0) {
Color.red
.frame(height:25)
Color.blue
.allowsHitTesting(false)
}
}
}
You can wrap the DragGesture into a ViewModifier:
struct MovableByBar: ViewModifier {
static let barHeight: CGFloat = 14
#State private var loc: CGPoint!
#State private var transition: CGSize = .zero
func body(content: Content) -> some View {
if loc == nil { // Get the original position
content.padding(.top, MovableByBar.barHeight)
.overlay {
GeometryReader { geo -> Color in
DispatchQueue.main.async {
let frame = geo.frame(in: .local)
loc = CGPoint(x: frame.midX, y: frame.midY)
}
return Color.clear
}
}
} else {
VStack(spacing: 0) {
Rectangle()
.fill(.secondary)
.frame(height: MovableByBar.barHeight)
.offset(x: transition.width, y: transition.height)
.gesture (
DragGesture()
.onChanged { value in
transition = value.translation
}
.onEnded { value in
loc.x += transition.width
loc.y += transition.height
transition = .zero
}
)
content
.offset(x: transition.width,
y: transition.height)
}
.position(loc)
}
}
}
And use modifier like this:
WindowView()
.modifier(MovableByBar())
.frame(width: 80, height: 60) // `frame()` after it

How can I create an animation color change from center of a view in SwiftUI?

How can I create an animation color change from center of a view in SwiftUI? (please see the below gif)?
So far I have tried adding an animation modifier to the view (please see the below code), but am not quite sure how to make the animation start at the center of the screen and flow to the outer edges of the screen.
import SwiftUI
struct ContentView: View {
#State var color = Color.red
var body: some View {
VStack {
Button(action: {
color = .green
}, label: {
Text("change background color")
})
}
.frame(maxWidth: .infinity)
.frame(maxHeight: .infinity)
.background(color)
.animation(.default)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can use two Circles in a ZStack and animate the scale effect value of the top most circle.
struct ContentView: View {
private let colors: [Color] = [.red, .yellow, .blue, .pink, .orange]
private let maxScaleEffect: CGFloat = 4.0
private let minScaleEffect: CGFloat = 0
private let animationDuration = 0.6
private let animationDelay = 0.1
#State private var shouldTransition = true
#State private var colorIndex = 0
var body: some View {
ZStack {
Circle()
.fill(previousColor)
.scaleEffect(maxScaleEffect)
Circle()
.fill(transitioningColor)
.scaleEffect(shouldTransition ? maxScaleEffect : minScaleEffect)
Button("Change color") {
shouldTransition = false
colorIndex += 1
// Removing DispatchQueue here will cause the first transition not to work
DispatchQueue.main.asyncAfter(deadline: .now() + animationDelay) {
withAnimation(.easeInOut(duration: animationDuration)) {
shouldTransition = true
}
}
}
.foregroundColor(.primary)
}
}
private var previousColor: Color {
colors[colorIndex%colors.count]
}
private var transitioningColor: Color {
colors[(colorIndex+1)%colors.count]
}
}

SwiftUI: Understanding .sheet / .fullScreenCover lifecycle when using constant vs #Binding initializers

I'm trying to understand how and when the .sheet and .fullScreenCover initializers are called. Below is a minimal reproducible example, where the first screen has 3 colored rectangles and the SecondView (shown via .fullScreenCover) has 1 rectangle that changes color based on the selected color from the first screen.
When the app first loads, the color is set to .gray.
If I tap on the green rectangle, SecondView presents with a gray rectangle. (ie. the color DIDN'T change correctly).
If I then dismiss the SecondView and tap on the red rectangle, the SecondView presents with a red rectangle. (ie. the color DID change correctly.)
So, I'm wondering why this set up does NOT work on the initial load, but does work on the 2nd/3rd try?
Note: I understand this can be solved by changing the 'let selectedColor' to a #Binding variable, that's not what I'm asking.
Code:
import SwiftUI
struct SegueTest: View {
#State var showSheet: Bool = false
#State var color: Color = .gray
var body: some View {
HStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.red)
.frame(width: 100, height: 100)
.onTapGesture {
color = .red
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.green)
.frame(width: 100, height: 100)
.onTapGesture {
color = .green
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.orange)
.frame(width: 100, height: 100)
.onTapGesture {
color = .orange
showSheet.toggle()
}
}
.fullScreenCover(isPresented: $showSheet, content: {
SecondView(selectedColor: color)
})
}
}
struct SecondView: View {
#Environment(\.presentationMode) var presentationMode
let selectedColor: Color // Should change to #Binding
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
RoundedRectangle(cornerRadius: 25)
.fill(selectedColor)
.frame(width: 300, height: 300)
}
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
}
struct SegueTest_Previews: PreviewProvider {
static var previews: some View {
SegueTest()
}
}
See comments and print statements. Especially the red
import SwiftUI
struct SegueTest: View {
#State var showSheet: Bool = false{
didSet{
print("showSheet :: didSet")
}
willSet{
print("showSheet :: willSet")
}
}
#State var color: Color = .gray{
didSet{
print("color :: didSet :: \(color.description)")
}
willSet{
print("color :: willSet :: \(color.description)")
}
}
#State var refresh: Bool = false
init(){
print("SegueTest " + #function)
}
var body: some View {
print(#function)
return HStack {
//Just to see what happens when you recreate the View
//Text(refresh.description)
Text(color.description)
RoundedRectangle(cornerRadius: 25)
.fill(Color.red)
.frame(width: 100, height: 100)
.onTapGesture {
print("SegueTest :: onTapGesture :: red")
//Changing the color
color = .red
//Refreshed SegueTest reloads function
//refresh.toggle()
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.green)
.frame(width: 100, height: 100)
.onTapGesture {
print("SegueTest :: onTapGesture :: green")
//Changing the color
color = .green
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.orange)
.frame(width: 100, height: 100)
.onTapGesture {
print("SegueTest :: onTapGesture :: orange")
//Changing the color
color = .orange
showSheet.toggle()
}
}
//This part is likely created when SegueTest is created and since a struct is immutable it keeps the original value
.fullScreenCover(isPresented: $showSheet, content: {
SecondView(selectedColor: color)
})
}
}
struct SecondView: View {
#Environment(\.presentationMode) var presentationMode
//struct is immutable
let selectedColor: Color // Should change to #Binding
init(selectedColor: Color){
print("SecondView " + #function)
self.selectedColor = selectedColor
print("SecondView :: struct :: selectedColor = \(self.selectedColor.description)" )
print("SecondView :: parameter :: selectedColor = \(selectedColor.description)" )
}
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
RoundedRectangle(cornerRadius: 25)
.fill(selectedColor)
.frame(width: 300, height: 300)
}
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
}
struct SegueTest_Previews: PreviewProvider {
static var previews: some View {
SegueTest()
}
}
The problem is that you are not using your #State color inside SegueView, which will not reload your view on State change. Just include your #State somewhere in the code, which will force it to rerender at then update the sheet, with the correct color.
RoundedRectangle(cornerRadius: 25)
.fill(color == .red ? Color.red : Color.red) //<< just use dump color variable here
If you do not include your color state somewhere in the code, it won't re render your SegueView, hence you still pass the old color gray to your SecondView.
Or even pass your colors as Binding your your Second View..
SecondView(selectedColor: $color)
#Binding var selectedColor: Color

SwiftUI View struct without reloading

I would like to create a starry background view in SwiftUI that has its stars located randomly using Double.random(), but does not reinitialise them and move them when the parent view reloads its var body.
struct ContentView: View {
#State private var showButton = true
var body: some View {
ZStack {
BackgroundView()
if showButton {
Button("Tap me"){
self.showButton = false
}
}
}
}
}
I define my background view as such.
struct BackgroundView: View {
var body: some View {
ZStack {
GeometryReader { geometry in
Color.black
ForEach(0..<self.getStarAmount(using: geometry), id: \.self){ _ in
Star(using: geometry)
}
LinearGradient(gradient: Gradient(colors: [.purple, .clear]), startPoint: .bottom, endPoint: .top)
.opacity(0.7)
}
}
}
func getStarAmount(using geometry: GeometryProxy) -> Int {
return Int(geometry.size.width*geometry.size.height/100)
}
}
A Star is defined as
struct Star: View {
let pos: CGPoint
#State private var opacity = Double.random(in: 0.05..<0.4)
init(using geometry: GeometryProxy) {
self.pos = CGPoint(x: Double.random(in: 0..<Double(geometry.size.width)), y: Double.random(in: 0..<Double(geometry.size.height)))
}
var body: some View {
Circle()
.foregroundColor(.white)
.frame(width: 2, height: 2)
.scaleEffect(CGFloat(Double.random(in: 0.25...1)))
.position(pos)
.opacity(self.opacity)
.onAppear(){
withAnimation(Animation.linear(duration: 2).delay(Double.random(in: 0..<6)).repeatForever()){
self.opacity = self.opacity+0.5
}
}
}
}
As one can see, a Star heavily relies on random values, for both its animation (to create a 'random' twinkling effect) as well as its position. When the parent view of the BackgroundView, ContentView in this example, gets redrawn however, all Stars get reinitialised, their position values change and they move across the screen. How can this best be prevented?
I have tried several approaches to prevent the positions from being reinitialised. I can create a struct StarCollection as a static let of BackgroundView, but this is quite cumbersome. What is the best way to go about having a View dependent on random values (positions), only determine those positions once?
Furthermore, the rendering is quite slow. I have attempted to call .drawingGroup() on the ForEach, but this then seems to interfere with the animation's opacity interpolation. Is there any viable way to speed up the creation / re-rendering of a view with many Circle() elements?
The slowness coming out from the overcomplicated animations setting in onAppear, you only need the self.opacity state change to initiate the animation, so please move animation out and add to the shape directly.
Circle()
.foregroundColor(.white)
.frame(width: 2, height: 2)
.scaleEffect(CGFloat(Double.random(in: 0.25...1)))
.position(pos)
.opacity(self.opacity)
.animation(Animation.linear(duration: 0.2).delay(Double.random(in: 0..<6)).repeatForever())
.onAppear(){
// withAnimation{ //(Animation.linear(duration: 2).delay(Double.random(in: 0..<6)).repeatForever()){
self.opacity = self.opacity+0.5
// }
}