SwiftUI - offset wrong or weird padding? - swiftui

I'm creating a schedule app and im trying to render the activities on a calendar sort of thing. Im doing this by having a set spacing of 20 between every hour mark. This means that to position the activities I can take the hour the activity start times this set height of 20 to position it correctly. This does indeed work but when I add multiple activities the offset is wrong? But if I render every activity on its own the offset is correct.
This is the code for displaying the activities.
struct Event: Hashable {
var name: String
var start: Int
var end: Int
var y: CGFloat
var eHeight: CGFloat
var color: Color
}
struct EventIndicatorView: View {
var day = [Event]()
init() {
day.append(createEvent(name: "Matematik", start: 8, end: 9, color: Color.blue))
day.append(createEvent(name: "Svenska", start: 10, end: 11, color: Color.green))
day.append(createEvent(name: "Fysik", start: 12, end: 14, color: Color.red))
day.append(createEvent(name: "Idrott", start: 15, end: 16, color: Color.pink))
}
func createEvent(name: String, start: Int, end: Int, color: Color) -> Event {
let eHeight = CGFloat((end - start) * height * 2)
//let y = CGFloat((start*height*2) + height)
let y = CGFloat((start*2*height)+height*(end-start))
let e = Event(name: name, start: start, end: end, y: y, eHeight: eHeight, color: color)
return e
}
var body: some View {
VStack(spacing: 0) {
ForEach(day, id: \.self) { d in
HStack() {
VStack(spacing: 0) {
HStack(spacing: 0) {
Rectangle()
.fill(d.color)
.frame(width:5, height: d.eHeight)
ZStack(alignment: .topLeading) {
Rectangle()
.fill(d.color.opacity(0.5))
.frame(height: d.eHeight)
Text(d.name)
.padding(EdgeInsets(top: 4, leading: 4, bottom: 0, trailing: 0))
.foregroundColor(d.color)
.font(.subheadline.bold())
}
}
.cornerRadius(4)
.offset(x: 60, y: d.y)
}
}
.frame(height: CGFloat(height))
}
Spacer()
}
}
}
This is the code for showing the hours.
var hours = [String]()
struct CalenderView: View {
init() {
for hour in 0...24 {
if hour == 24 {
hours.append("00:00")
return
}
if hour <= 9 {
hours.append("0\(hour):00")
} else {
hours.append("\(hour):00")
}
}
}
var body: some View {
VStack(spacing: CGFloat(height)) {
ForEach(hours.indices, id:\.self) {index in
HStack(spacing: 10) {
Text(hours[index])
.frame(width: 40, height: CGFloat(height))
.font(.caption.bold())
.foregroundColor(Color(.systemGray))
Rectangle()
.fill(Color(.systemGray4))
.frame(height: 1)
}
.padding(.leading, 10)
}
}
}
}
This code results in this:
Display of multiple acctiviys
The green one should be positioned on 10 but isn't.
Here is a picture where I only display one activity:
Only one activity
But if I only display the green one its position is correct

Related

How can I display tabular data with SwiftUI on iPhone?

I'm working on an app for my local sports league.
One view will be the current standings, with several fields on each row: Name of team, Games Played, Points, etc. I want the Teams column to be left-aligned, the other columns to be right-aligned.
It seems the best answer is SwiftUI's Table(), but on iPhone, it only displays the first column.
I've tried using an HStack{} with Text():
List {
ForEach(selectedStats()) { stats in
HStack {
Text (stats.name)
Spacer()
Text ("\(stats.totalMatches)")
Text ("\(stats.standingsPoints)")
}
}
}
but I'm having difficulty getting the columns to align left or right across multiple lines in the table.
Is there a way to get Tables to work? Or how can I align my columns?
(P. S. I used the tag TableView, because there is no tag specific to Table or SwiftUI-Table. It would be nice if that could be added.)
SwiftUI's Grid and GridRow are your friends for laying out simple content like this:
struct Stat: Identifiable, Equatable {
var id: String { name }
let name: String
let totalMatches: Int
let standingsPoints: Int
}
struct ContentView: View {
let stats = [
Stat(name: "Fred", totalMatches: 12, standingsPoints: 87),
Stat(name: "Jim", totalMatches: 4, standingsPoints: 12),
Stat(name: "Dave", totalMatches: 9, standingsPoints: 91)]
var body: some View {
List {
Grid {
GridRow {
Text("Name")
Text("Matches")
Text("Points")
}
.bold()
Divider()
ForEach(stats) { stat in
GridRow {
Text(stat.name)
Text(stat.totalMatches, format: .number)
Text(stat.standingsPoints, format: .number)
}
if stat != stats.last {
Divider()
}
}
}
}
}
}
You can tweak the layout by adding Spacer()s .gridCellAnchor modifiers, e.g.
var body: some View {
List {
Grid {
GridRow {
Text("Name"). // UnitPoint(x: 1, y: 0.5) = align right
.gridCellAnchor(UnitPoint(x: 1, y: 0.5))
Spacer()
Text("Matches") // UnitPoint(x: 0, y: 0.5) = align left
.gridCellAnchor(UnitPoint(x: 0, y: 0.5))
Text("Points")
.gridCellAnchor(UnitPoint(x: 0, y: 0.5))
}
.bold()
Divider()
ForEach(stats) { stat in
GridRow {
Text(stat.name)
.gridCellAnchor(UnitPoint(x: 1, y: 0.5))
Spacer()
Text(stat.totalMatches, format: .number)
.gridCellAnchor(UnitPoint(x: 0, y: 0.5))
Text(stat.standingsPoints, format: .number)
.gridCellAnchor(UnitPoint(x: 0, y: 0.5))
}
if stat != stats.last {
Divider()
}
}
}
}
}
I kept researching after I posted and came across some stuff by Paul Hudson of Hacking with Swift fame.
Alignment and alignment guides
How to create a custom alignment guide
Based on those two, I came up with this:
extension HorizontalAlignment {
enum GP: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[.trailing]
}
}
static let gp = HorizontalAlignment(GP.self)
}
and then I apply
.alignmentGuide(.gp) { d in d[HorizontalAlignment.trailing] }
to each element I want to right-align.
Seems to do the trick, as well.

How to animate a view in a circular motion using its real-time position coordinates?

I'm currently working on a SwiftUI project, and in order to detect intersections/collisions, I need real-time coordinates, which SwiftUI animations cannot offer. After doing some research, I came across a wonderful question by Kike regarding how to get the real-time coordinates of a view when it is moving/transitioning. And Pylyp Dukhov's answer to that topic recommended utilizing CADisplayLink to calculate the position for each frame and provided a workable solution that did return the real time values when transitioning.
But I'm so unfamiliar with CADisplayLink and creating custom animations that I'm not sure I'll be able to bend it to function the way I want it to.
So this is the animation I want to achieve using CADisplayLink that animates the orange circle view in a circular motion using its position coordinates and repeats forever:
Here is the SwiftUI code:
struct CircleView: View {
#Binding var moveClockwise: Bool
#Binding var duration: Double // Works as speed, since it repeats forever
let geo: GeometryProxy
var body: some View {
ZStack {
Circle()
.stroke()
.frame(width: geo.size.width, height: geo.size.width, alignment: .center)
//MARK: - What I have with SwiftUI animation
Circle()
.fill(.orange)
.frame(width: 35, height: 35, alignment: .center)
.offset(x: -CGFloat(geo.size.width / 2))
.rotationEffect(.degrees(moveClockwise ? 360 : 0))
.animation(
.linear(duration: duration)
.repeatForever(autoreverses: false), value: moveClockwise
)
//MARK: - What I need with CADisplayLink
// Circle()
// .fill(.orange)
// .frame(width: 35, height: 35, alignment: .center)
// .position(CGPoint(x: pos.realTimeX, y: realTimeY))
Button("Start Clockwise") {
moveClockwise = true
// pos.startMovement
}.foregroundColor(.orange)
}.fixedSize()
}
}
struct ContentView: View {
#State private var moveClockwise = false
#State private var duration = 2.0 // Works as speed, since it repeats forever
var body: some View {
VStack {
GeometryReader { geo in
CircleView(moveClockwise: $moveClockwise, duration: $duration, geo: geo)
}
}.padding(20)
}
}
This is what I have currently with CADisplayLink, I added the coordinates to make a circle and that’s about it & it doesn’t repeat forever like the gif does:
Here is the CADisplayLink + real-time coordinate version that I’ve tackled and got lost:
struct Point: View {
var body: some View {
Circle()
.fill(.orange)
.frame(width: 35, height: 35, alignment: .center)
}
}
struct ContentView: View {
#StateObject var P: Position = Position()
var body: some View {
VStack {
ZStack {
Circle()
.stroke()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width, alignment: .center)
Point()
.position(x: P.realtimePosition.x, y: P.realtimePosition.y)
}
Text("X: \(P.realtimePosition.x), Y: \(P.realtimePosition.y)")
}.onAppear() {
P.startMovement()
}
}
}
class Position: ObservableObject, Equatable {
struct AnimationInfo {
let startDate: Date
let duration: TimeInterval
let startPoint: CGPoint
let endPoint: CGPoint
func point(at date: Date) -> (point: CGPoint, finished: Bool) {
let progress = CGFloat(max(0, min(1, date.timeIntervalSince(startDate) / duration)))
return (
point: CGPoint(
x: startPoint.x + (endPoint.x - startPoint.x) * progress,
y: startPoint.y + (endPoint.y - startPoint.y) * progress
),
finished: progress == 1
)
}
}
#Published var realtimePosition = CGPoint.zero
private var mainTimer: Timer = Timer()
private var executedTimes: Int = 0
private lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkAction))
displayLink.add(to: .main, forMode: .default)
return displayLink
}()
private let animationDuration: TimeInterval = 0.1
private var animationInfo: AnimationInfo?
private var coordinatesPoints: [CGPoint] {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
// great progress haha
let radius: Double = Double(screenWidth / 2)
let center = CGPoint(x: screenWidth / 2, y: screenHeight / 2)
var coordinates: [CGPoint] = []
for i in stride(from: 1, to: 360, by: 10) {
let radians = Double(i) * Double.pi / 180 // raiments = degrees * pI / 180
let x = Double(center.x) + radius * cos(radians)
let y = Double(center.y) + radius * sin(radians)
coordinates.append(CGPoint(x: x, y: y))
}
return coordinates
}
// Conform to Equatable protocol
static func ==(lhs: Position, rhs: Position) -> Bool {
// not sure why would you need Equatable for an observable object?
// this is not how it determines changes to update the view
if lhs.realtimePosition == rhs.realtimePosition {
return true
}
return false
}
func startMovement() {
mainTimer = Timer.scheduledTimer(
timeInterval: 0.1,
target: self,
selector: #selector(movePoint),
userInfo: nil,
repeats: true
)
}
#objc func movePoint() {
if (executedTimes == coordinatesPoints.count) {
mainTimer.invalidate()
return
}
animationInfo = AnimationInfo(
startDate: Date(),
duration: animationDuration,
startPoint: realtimePosition,
endPoint: coordinatesPoints[executedTimes]
)
displayLink.isPaused = false
executedTimes += 1
}
#objc func displayLinkAction() {
guard
let (point, finished) = animationInfo?.point(at: Date())
else {
displayLink.isPaused = true
return
}
realtimePosition = point
if finished {
displayLink.isPaused = true
animationInfo = nil
}
}
}
Inside Position you're calculating position related to whole screen. But .position modifier requires value related to the parent view size.
You need to make your calculations based on the parent size, you can use such sizeReader for this purpose:
extension View {
func sizeReader(_ block: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometry in
Color.clear
.onAppear {
block(geometry.size)
}
.onChange(of: geometry.size, perform: block)
}
)
}
}
Usage:
ZStack {
Circle()
.stroke()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width)
Point()
.position(x: P.realtimePosition.x, y: P.realtimePosition.y)
}
.sizeReader { size in
P.containerSize = size
}
Also CADisplayLink is not used in the right way. The whole point of this tool is that it's already called on each frame, so you can calculate real time position, so your animation is gonna be really smooth, and you don't need a timer or pre-calculated values for only 180(or any other number) positions.
In the linked answer timer was used because a delay was needed between animations, but in your case the code can be greatly simplified:
class Position: ObservableObject {
#Published var realtimePosition = CGPoint.zero
var containerSize: CGSize?
private lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkAction))
displayLink.add(to: .main, forMode: .default)
displayLink.isPaused = true
return displayLink
}()
private var startDate: Date?
func startMovement() {
startDate = Date()
displayLink.isPaused = false
}
let animationDuration: TimeInterval = 5
#objc func displayLinkAction() {
guard
let containerSize = containerSize,
let timePassed = startDate?.timeIntervalSinceNow,
case let progress = -timePassed / animationDuration,
progress <= 1
else {
displayLink.isPaused = true
startDate = nil
return
}
let frame = CGRect(origin: .zero, size: containerSize)
let radius = frame.midX
let radians = CGFloat(progress) * 2 * .pi
realtimePosition = CGPoint(
x: frame.midX + radius * cos(radians),
y: frame.midY + radius * sin(radians)
)
}
}
I've tried to make more simplified the implementation, here is the SwiftUI code,
struct RotatingDotAnimation: View {
#State private var moveClockwise = false
#State private var duration = 1.0 // Works as speed, since it repeats forever
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 4)
.foregroundColor(.white.opacity(0.5))
.frame(width: 150, height: 150, alignment: .center)
Circle()
.fill(.white)
.frame(width: 18, height: 18, alignment: .center)
.offset(x: -63)
.rotationEffect(.degrees(moveClockwise ? 360 : 0))
.animation(.easeInOut(duration: duration).repeatForever(autoreverses: false),
value: moveClockwise
)
}
.onAppear {
self.moveClockwise.toggle()
}
}
}
It'll basically create animation like this,
enter image description here

How to animate color change swiftui

I am trying to animate the color transition between green, yellow, and red. How would I go about this? I have tried using animation() on the Circle view, but this created weird bugs in which the StrokeStyle seems to be ignored and the whole circle fills with color during the transition.
import SwiftUI
import UserNotifications
struct CircleTimer_Previews: PreviewProvider {
static var previews: some View {
Group {
CircleTimer() }
}
}
struct CircleTimer : View {
#State var to: CGFloat = 1;
#State var start = false
#State var count = 15
let defaultTime = 15
#State var time = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View{
ZStack{
Color.black.opacity(0.06).edgesIgnoringSafeArea(.all)
VStack{
ZStack{
Circle()
.trim(from: 0, to: 1)
.stroke(Color.black.opacity(0.09), style: StrokeStyle(lineWidth: 35, lineCap: .round))
.frame(width: 280, height: 280)
Circle()
.trim(from: 0, to: self.to)
.stroke(self.count > 10 ? Color.green : self.count > 5 ? Color.yellow : Color.red, style: StrokeStyle(lineWidth: 35, lineCap: .round))
.frame(width: 280, height: 280)
.rotationEffect(.init(degrees: -90))
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
VStack{
Text("\(self.count)")
.font(.system(size: 72))
.fontWeight(.bold)
}
}
TimerButtons(start: $start, to: $to, count: $count)
}
}
.onAppear(perform: {
UNUserNotificationCenter.current().requestAuthorization(options: [.badge,.sound,.alert]) { (_, _) in
}
})
.onReceive(self.time) { (_) in
if self.start{
if self.count > 0 {
self.count -= 1
print(self.count)
withAnimation(.default){
self.to = CGFloat(self.count) / 15
print(self.to)
}
}
else{
self.start.toggle()
}
}
}
}
}
As suggested by #Yrb, you must create three separate views of three different colors.

Positioning Views in ForEach SwiftUI

I would like to add animating views to a parent view. I know that the parent view needs to position the children but I'm having trouble coming up with the formula to implement. I have the first couple of views right but once I get to 4 and up its a problem! I would like the views to appear in a grid with 3 columns.
Here is some reproducible code ready to be copy and pasted.
import SwiftUI
struct CustomView: View, Identifiable {
#State private var startAnimation = false
let id = UUID()
var body: some View {
Circle()
.frame(width: 50, height: 50)
.scaleEffect(x: startAnimation ? 2 : 1,
y: startAnimation ? 2 : 1)
.animation(Animation.interpolatingSpring(mass: 2, stiffness: 20, damping: 1, initialVelocity: 1))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.startAnimation = true
}
}
}
}
struct StartView: View {
#State private var userSelection: [CustomView] = []
var body: some View {
VStack(spacing: -20) {
Button("Add View") {
self.userSelection.append(CustomView())
}
LazyVGrid(columns: gridStyle) {
ForEach(Array(userSelection.enumerated()), id: \.0 ){ index, equip in
CustomView()
.position(x: widthBasedOn(index: index), y: heightBasedOn(index: index))
}
.padding([])
}
.frame(width: UIScreen.main.bounds.width * 0.5,
height: UIScreen.main.bounds.height * 0.8)
}
}
let gridStyle = [
GridItem(.flexible(minimum: 0, maximum: 100), spacing: -50),
GridItem(.flexible(minimum: 0, maximum: 100), spacing: -50),
GridItem(.flexible(minimum: 0, maximum: 100), spacing: -50)
]
private func widthBasedOn(index: Int) -> CGFloat {
if index % 3 != 0 {
if index > 3 {
let difference = index - 4
return CGFloat(index * difference * 100)
}
let answer = CGFloat(index * 100)
print("\(index) width should be: \(answer)")
return answer
}
return 0
}
private func heightBasedOn(index: Int) -> CGFloat {
if index > 3 && index < 6 {
return 100
}
return 200
}
}
struct EquipmentSelectionView_Previews: PreviewProvider {
static var previews: some View {
StartView()
}
}
Since most of your question is somewhat vague, and I am not sure about the specifics, this is my solution. Feel free to respond, and I will be glad to answer your question further with more tailored solution.
I removed many of your code that was unnecessary or overly-complicated. For example, I removed the widthBasedOn and heightBasedOn methods. I also changed the array property var userSelection: [CustomView] to var numberOfViews = 0.
Note: Both your original code and my solution cause all the circles to wiggle up and down, whenever a new circle is added.
I suggest that you copy paste this code snippet, run it in Xcode, and see if this is what you want.
struct CustomView: View, Identifiable {
#State private var startAnimation = false
let id = UUID()
var body: some View {
Circle()
//Changing the frame size of the circle, making it bigger or smaller
.frame(width: startAnimation ? 100 : 50, height: startAnimation ? 100 : 50)
.animation(Animation.interpolatingSpring(mass: 2, stiffness: 20, damping: 1, initialVelocity: 1))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.startAnimation = true
}
}
}
}
struct StartView: View {
//View will display this number of circles
#State private var numberOfViews = 0
var body: some View {
VStack() {
Button("Add View") {
self.numberOfViews += 1
}
.padding(.top, 100)
Spacer()
LazyVGrid(columns: gridStyle) {
//Add a new circle CustomView() to the LazyVGrid for each number of views
ForEach(0..<numberOfViews, id: \.self ){view in
CustomView()
}
}
}
}
//3 columns, flexible spacing for elments. In this case, equal amount of spacing.
let gridStyle = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
]
}
struct EquipmentSelectionView_Previews: PreviewProvider {
static var previews: some View {
StartView()
}
}
Limiting number of circles
To limit the number of circles:
if numberOfViews < 9 {
self.numberOfViews += 1
}
Positioning the button
To position the button, you can add padding:
Button("Add View") {
if numberOfViews < 9 {
self.numberOfViews += 1
}
}
.padding(.top, 100)
Overlap vs. No Overlap
Using there .frame modifier will not have any overlap:
.frame(width: startAnimation ? 100 : 50, height: startAnimation ? 100 : 50)
But if you do want overlap, use .scaleEffect:
.scaleEffect(x: startAnimation ? 2 : 1,
y: startAnimation ? 2 : 1)
P.S. Unfortunately, I can't show you the results with GIF images because Stackoverflow keep giving me upload errors.

How to animate views in turn

I want to animate the arriving of views on the screen, in turn, one by one. Now my application draws circles and they arrive on the screen at the same time. But I would like it if the first circle will take its position and only after this the second circle will start its animation and etc. What solutions does this problem have?
import SwiftUI
struct ContentView: View {
var body: some View {
GenrealView()
}
}
struct GenrealView: View {
#State var hide = false
var body: some View {
giveViewForBody()
}
func giveViewForBody() -> some View {
ZStack {
drawCircles()
Button(action: {
self.hide.toggle()
}) {
Text(hide ? "Show circles" : "Hide circles")
}.padding(50)
}
}
func drawCircles(times: Int = 4) -> some View {
ForEach(0..<times) { _ in
Circle()
.fill(Color.green)
.frame(width: 100, height: 100)
.position(x: -100, y: -100)
.offset(x: CGFloat(hide ? 0 : 100 + Int.random(in: 100...300)),
y: CGFloat(hide ? 0 : 200 + Int.random(in: 50...600)))
.animation(.easeIn(duration: 2.0))
}
}
}
Add a .delay to each Circle() and make the delay larger for each successive one. Add index in to your ForEach loop and then make the delay .delay(2.0 * Double(index)):
func drawCircles(times: Int = 4) -> some View {
ForEach(0..<times) { index in
Circle()
.fill(Color.green)
.frame(width: 100, height: 100)
.position(x: -100, y: -100)
.offset(x: CGFloat(hide ? 0 : 100 + Int.random(in: 100...300)),
y: CGFloat(hide ? 0 : 200 + Int.random(in: 50...600)))
.animation(Animation.easeIn(duration: 2.0).delay(2.0 * Double(index)))
}
}