I have created a carousel cards in SwiftUI, it is working on the DragGesture
I want to achieve same experience using scrollview i.e. same design and functionalities using scrollview instead of Drag-gesture
I have created Sample using scrollview but it has some limitation
Here is ScreenShot the upper carousel is using scrollview and lower one using Drag Gesture
import SwiftUI
struct Item: Identifiable {
var id: Int
var title: String
var color: Color
var isSelected: Bool
}
class Store: ObservableObject {
#Published var items: [Item]
let colors: [Color] = [.red, .orange, .blue, .teal, .mint, .green, .gray, .indigo,.red, .orange, .blue, .teal, .mint, .green, .gray, .indigo]
init() {
items = []
for i in 0...15 {
let new = Item(id: i, title: "Item \(i)", color: colors[i], isSelected: false)
items.append(new)
}
}
}
struct ContentView: View {
#StateObject var store = Store()
#State private var draggingItem = 0.0
#State var activeIndex: Int = 0
#State var selectedIndex: Int = 0
#State private var snappedItem = 0.0
let gridItems = [
GridItem(.flexible())
]
var body: some View {
VStack {
Spacer()
Text("Selected Index: \(store.items[selectedIndex].id)")
.fontWeight(.bold)
.padding()
Text("Acticted Index: \(activeIndex)")
.fontWeight(.bold)
.padding()
Spacer()
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { scrollview in
LazyHGrid(rows: gridItems, alignment: .center, spacing: 25) {
ForEach(0..<store.items.count, id: \.self) { index in
GeometryReader { proxy in
let scale = getScale(proxy: proxy)
ZStack() {
Circle()
.fill(store.items[index].color)
Text(store.items[index].title)
.font(.body)
.fontWeight(.light)
}.frame(width: 70, height: 70)
.onTapGesture {
withAnimation {
print("Color: ",store.items[index].color)
print("ID: ",store.items[index].id)
print("Title: ",store.items[index].title)
selectedIndex = index
draggingItem = Double(selectedIndex)
activeIndex = selectedIndex
}
}
.overlay(Circle()
.stroke(selectedIndex == index ? .black : .clear, lineWidth: selectedIndex == index ? 2 : 0))
.scrollSnappingAnchor(.bounds)
.scaleEffect(.init(width: (scale * 1.2) , height: (scale * 1.2)))
.animation(.easeOut(duration: 0.2), value: 0)
.padding(.vertical)
.onChange(of: selectedIndex) { newValue in
withAnimation {
scrollview.scrollTo(selectedIndex, anchor: .center)
}
}
.zIndex(1.0 - abs(distance(store.items[index].id)) * 0.1)
} //End Geometry
.frame(width: 70, height: 150)
} //End ForEach
} //End Grid
}
}
ZStack {
ForEach(0..<store.items.count, id: \.self) { index in
ZStack {
Circle()
.fill(store.items[index].color)
Text(store.items[index].title)
.padding()
}
.frame(width: 100, height: 100)
.onTapGesture { loc in
print("Color: ",store.items[index].color)
print("ID: ",store.items[index].id)
print("Title: ",store.items[index].title)
selectedIndex = index
withAnimation(.linear) {
draggingItem = Double(store.items[index].id)
activeIndex = index
}
}
.overlay(Circle()
.stroke(activeIndex == index ? .white : .clear, lineWidth: activeIndex == index ? 2 : 0))
.scaleEffect(1.0 - abs(distance(store.items[index].id)) * 0.15 )
.offset(x: myXOffset(store.items[index].id), y: 0)
.zIndex(1.0 - abs(distance(store.items[index].id)) * 0.1)
}
}
.gesture(
DragGesture()
.onChanged { value in
draggingItem = (snappedItem) + value.translation.width / 100
}
.onEnded { value in
withAnimation {
snappedItem = draggingItem
draggingItem = round(draggingItem).remainder(dividingBy: Double(store.items.count))
//Get the active Item index
self.activeIndex = store.items.count + Int(draggingItem)
if self.activeIndex > store.items.count || Int(draggingItem) >= 0 {
self.activeIndex = Int(draggingItem)
}
}
}
)
}
}
func distance(_ item: Int) -> Double {
return (draggingItem - Double(item)).remainder(dividingBy: Double(store.items.count))
}
func myXOffset(_ item: Int) -> Double {
let angle = Double.pi * 2 / Double(store.items.count) * distance(item)
return sin(angle) * 200
}
func getScale(proxy: GeometryProxy) -> CGFloat {
let midPoint: CGFloat = 200
let viewFrame = proxy.frame(in: CoordinateSpace.global)
var scale: CGFloat = 1.0
let deltaXAnimationThreshold: CGFloat = 70
let diffFromCenter = abs(midPoint - viewFrame.origin.x - deltaXAnimationThreshold / 2)
if diffFromCenter < deltaXAnimationThreshold {
scale = 1 + (deltaXAnimationThreshold - diffFromCenter) / 300
}
return scale
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Related
What I'm trying to do is basically having some images in a TabView and being able to zoom and pan on those images.
Right now I got this quite nicely implemented but I struggle to find a solution so that it is not possible to pan outside the bounds of the image.
This is what I currently have:
I want to try having the image clip to the side of the screen so you don't pan the image out of the visible area.
So far this it what my code looks like:
struct FinalImageSwipeView: View {
#ObservedObject var viewModel: ImageChatViewModel
#State var currentImage: UUID
#State var fullPreview: Bool = false
#GestureState var draggingOffset: CGSize = .zero
var body: some View {
TabView(selection: $currentImage) {
ForEach(viewModel.images) { image in
GeometryReader{ proxy in
let size = proxy.size
PinchAndPanImage(image: UIImage(named: image.imageName)!,
fullPreview: $fullPreview)
.frame(width: size.width, height: size.height)
.contentShape(Rectangle())
}
.tag(image.id)
.ignoresSafeArea()
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.ignoresSafeArea()
// upper navigation bar
.overlay(
ImageSwipeViewNavigationBar(fullPreview: $fullPreview, hideSwipeView: viewModel.hideSwipeView),
alignment: .top
)
// bottom image scrollview
.overlay(
ImageSwipeViewImageSelection(viewModel: viewModel,
currentImage: $currentImage,
fullPreview: $fullPreview),
alignment: .bottom
)
.gesture(DragGesture().updating($draggingOffset, body: { (value, outValue, _) in
if viewModel.imageScale == 0 {
outValue = value.translation
viewModel.onChangeDragGesture(value: draggingOffset)
}}).onEnded({ (value) in
if viewModel.imageScale == 0 {
viewModel.onEnd(value: value)
}
}))
.transition(.offset(y: UIScreen.main.bounds.size.height + 100))
}
}
struct ImageSwipeViewImageSelection: View {
#ObservedObject var viewModel: ImageChatViewModel
#Binding var currentImage: UUID
#Binding var fullPreview: Bool
var body: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: true) {
HStack(spacing: 15) {
ForEach(viewModel.images) { image in
Image(image.imageName)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 70, height: 60)
.cornerRadius(12)
.id(image.id)
.overlay {
RoundedRectangle(cornerRadius: 12)
.stroke(image.id == currentImage ? Color.white : Color.clear, lineWidth: 2)
}
.onTapGesture {
currentImage = image.id
}
}
}
.padding()
}
.frame(height: 80)
.background(BlurView(style: .systemUltraThinMaterialDark).ignoresSafeArea(edges: .bottom))
// While current post changing center current image in scrollview
.onAppear(perform: {
proxy.scrollTo(currentImage, anchor: .bottom)
})
.onChange(of: currentImage) { _ in
viewModel.imageScale = 1
withAnimation {
proxy.scrollTo(currentImage, anchor: .bottom)
}
}
}
.offset(y: fullPreview ? 150 : 0)
}
}
struct PinchAndPanImage: View {
let image: UIImage
#Binding var fullPreview: Bool
// Stuff for Pinch and Pan
#State var imageScale: CGFloat = 1
#State var imageCurrentScale: CGFloat = 0
#State var imagePanOffset: CGSize = .zero
#State var currentImagePanOffset: CGSize = .zero
var usedImageScale: CGFloat {
max(1, min(imageScale + imageCurrentScale, 10))
}
var usedImagePan: CGSize {
let width = imagePanOffset.width + currentImagePanOffset.width
let height = imagePanOffset.height + currentImagePanOffset.height
return CGSize(width: width, height: height)
}
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(0)
.offset(usedImagePan)
.scaleEffect(usedImageScale > 1 ? usedImageScale : 1)
.gesture(
// Magnifying Gesture
MagnificationGesture()
.onChanged({ value in
imageCurrentScale = value - 1
})
.onEnded({ value in
imageCurrentScale = 0
imageScale = imageScale + value - 1
withAnimation(.easeInOut) {
if imageScale > 5 {
imageScale = 5
}
}
})
)
.simultaneousGesture(createPanGesture())
.onTapGesture(count: 2) {
withAnimation {
imageScale = 1
imagePanOffset = .zero
}
}
.onTapGesture(count: 1) {
withAnimation {
fullPreview.toggle()
}
}
}
private func createPanGesture() -> _EndedGesture<_ChangedGesture<DragGesture>>? {
let gesture = DragGesture()
.onChanged { value in
let width = value.translation.width / usedImageScale
let height = value.translation.height / usedImageScale
currentImagePanOffset = CGSize(width: width, height: height)
}
.onEnded { value in
currentImagePanOffset = .zero
let scaledWidth = value.translation.width / usedImageScale
let scaledHeight = value.translation.height / usedImageScale
let width = imagePanOffset.width + scaledWidth
let height = imagePanOffset.height + scaledHeight
imagePanOffset = CGSize(width: width, height: height)
}
return imageScale > 1 ? gesture : nil
}
}
I'm trying to make animation when I tap on view it becomes full screen and when I drag down view it scales down and returns to its previous state. I use matchedGeometryEffect with two views and change destination view frame with DragGesture, transition to source view works «unexpectedly». How to fix it or how to make this animation correct? If I don't change destination view frame it works as I expect (click button). GIF here: https://i.stack.imgur.com/yXsjF.gif
struct Test4: View {
#Namespace var animation
#State private var show = false
#State private var scale: CGFloat = 1
var body: some View {
ZStack(alignment: .topTrailing) {
if show {
ScrollView {
Color.gray
.border(Color.black, width: 30)
.matchedGeometryEffect(id: "animation", in: animation)
.frame(
width: UIScreen.main.bounds.width * scale,
height: UIScreen.main.bounds.height * scale)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged(onChanged)
.onEnded(onEnded)
)
}
.ignoresSafeArea()
Button("go back") {
withAnimation(Animation.easeInOut(duration: 1)) {
self.show.toggle()
}
}
} else {
VStack {
Color.gray
.border(Color.black, width: 30)
.matchedGeometryEffect(id: "animation", in: animation)
.frame(width: 200, height: 200)
.onTapGesture {
withAnimation {
self.scale = 1
show.toggle()
}
}
}
}
}
}
func onChanged(value: DragGesture.Value) {
withAnimation(Animation.easeInOut(duration: 3)) {
let currentScale = value.translation.height / UIScreen.main.bounds.height
if currentScale <= 0 {
return
}
let newScale = 1 - currentScale
if newScale > 0.85 {
self.scale = newScale
} else if newScale < 0.85 {
self.show = false
}
}
}
func onEnded(value: DragGesture.Value) {
withAnimation(Animation.easeInOut(duration: 3)) {
if self.scale < 0.85 {
self.show = false
}
}
}
}
struct Test4_Previews: PreviewProvider {
static var previews: some View {
Test4()
}
}
The thing that you are trying get is possible without matchedGeometryEffect, here is a simple approach:
import SwiftUI
struct ContentView: View {
var body: some View {
ScaleView()
}
}
struct ScaleView: View {
#State private var translation: CGFloat = CGFloat()
#State private var lastTranslation: CGFloat = CGFloat()
var body: some View {
GeometryReader { geometry in
Color.black
ZStack {
Color
.gray
Image(systemName: "arrow.triangle.2.circlepath.circle")
.font(Font.largeTitle)
.onTapGesture {
if lastTranslation == 400.0 {
lastTranslation = 0.0
translation = lastTranslation
}
else {
lastTranslation = 400.0
translation = lastTranslation
}
}
}
.position(x: geometry.size.width/2, y: geometry.size.height/2)
.cornerRadius(30)
.scaleEffect(1.0 - translation/(geometry.size.width > geometry.size.height ? geometry.size.width : geometry.size.height))
.gesture(DragGesture(minimumDistance: 0.0).onChanged(onChanged).onEnded(onEnded))
}
.ignoresSafeArea()
.animation(.easeInOut(duration: 0.35))
.statusBar(hidden: true)
}
func onChanged(value: DragGesture.Value) { translation = lastTranslation + value.translation.height }
func onEnded(value: DragGesture.Value) {
if value.translation.height > 0.0 {
lastTranslation = 400.0
translation = lastTranslation
}
else {
lastTranslation = 0.0
translation = lastTranslation
}
}
}
I have a movable VStack which carry a Picker. When I want chose deferent option from Picker I cannot, because SwiftUI thinks I want use DragGesture, therefor my Picker is lockdown! My DragGesture has minimumDistance: 0 but it does not solve issue when I change this value also, from other hand I like to have minimumDistance: 0 so it is not even an option for me to solving issue with increasing minimumDistance, so I need help to find a way, thanks.
struct ContentView: View {
var body: some View {
StyleView()
}
}
struct StyleView: View {
#State private var location: CGSize = CGSize()
#GestureState private var translation: CGSize = CGSize()
#State private var styleIndex: Int = 0
let styles: [String] = ["a", "b", "c"]
var body: some View {
VStack {
Picker(selection: $styleIndex, label: Text("Style")) {
ForEach(styles.indices, id:\.self) { index in
Text(styles[index].description)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
Text("selected style: " + styles[styleIndex])
}
.padding()
.background(Color.red)
.cornerRadius(10)
.padding()
.position(x: location.width + translation.width + 200, y: location.height + translation.height + 100)
.gesture(DragGesture(minimumDistance: 0)
.updating($translation) { value, state, _ in
state = value.translation
}
.onEnded { value in
location = CGSize(width: location.width + value.translation.width, height: location.height + value.translation.height)
})
}
}
DragGesture triggers when the user presses down on a view and move at least a certain distance away. So, this creates a picker with a drag gesture that triggers when the user moves it at least 10 points (it should be greater than 0 otherwise how can it know about the tap and the drag)
struct StyleView: View {
#State private var location: CGSize = CGSize()
#GestureState private var translation: CGSize = CGSize()
#State private var styleIndex: Int = 0
let styles: [String] = ["a", "b", "c"]
var body: some View {
VStack {
Picker(selection: $styleIndex, label: Text("Style")) {
ForEach(styles.indices, id:\.self) { index in
Text(styles[index].description)
}
}.gesture(DragAndTapGesture(count: styles.count, selected: $styleIndex).horizontal)
.pickerStyle(SegmentedPickerStyle())
.padding()
Text("selected style: " + styles[styleIndex])
}
.padding()
.background(Color.red)
.cornerRadius(10)
.padding()
.position(x: location.width + translation.width + 200, y: location.height + translation.height + 100)
.gesture(DragGesture(minimumDistance: 1)
.updating($translation) { value, state, _ in
state = value.translation
}
.onEnded { value in
location = CGSize(width: location.width + value.translation.width, height: location.height + value.translation.height)
})
}
}
struct DragAndTapGesture {
var count: Int
#Binding var selected: Int
init(count: Int, selected: Binding<Int>) {
self.count = count
self._selected = selected
}
var horizontal: some Gesture {
DragGesture().onEnded { value in
if -value.predictedEndTranslation.width > UIScreen.main.bounds.width / 2, self.selected < self.count - 1 {
self.selected += 1
}
if value.predictedEndTranslation.width > UIScreen.main.bounds.width / 2, self.selected > 0 {
self.selected -= 1
}
}
}
}
I want to drag across rectangles in a grid and change their color. This code is almost working, but this is not always the right rectangle that reacts: it behaves rather randomly. Any hints?
import SwiftUI
struct ContentView: View {
let data = (0...3)
#State private var colors: [Color] = Array(repeating: Color.gray, count: 4)
#State private var rect = [CGRect]()
var columns: [GridItem] =
Array(repeating: .init(.fixed(70), spacing: 1), count: 2)
var body: some View {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(data, id: \.self) { item in
Rectangle()
.fill(colors[item])
.frame(width: 70, height: 70)
.overlay(
GeometryReader{ geo in
Color.clear
.onAppear {
rect.insert(geo.frame(in: .global), at: rect.endIndex)
}
}
)
}
}
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged({ (value) in
if let match = rect.firstIndex(where: { $0.contains(value.location) }) {
colors[match] = Color.red
}
})
)
}
}
If I correctly understood your goal, here is fixed variant (with small modifications to avoid repeated hardcoding).
Tested with Xcode 12.1 / iOS 14.1
struct ContentView: View {
let data = (0...3)
#State private var colors: [Color]
#State private var rect: [CGRect]
init() {
_colors = State(initialValue: Array(repeating: .gray, count: data.count))
_rect = State(initialValue: Array(repeating: .zero, count: data.count))
}
var columns: [GridItem] =
Array(repeating: .init(.fixed(70), spacing: 1), count: 2)
var body: some View {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(data, id: \.self) { item in
Rectangle()
.fill(colors[item])
.frame(width: 70, height: 70)
.overlay(
GeometryReader{ geo in
Color.clear
.onAppear {
rect[item] = geo.frame(in: .global)
}
}
)
}
}
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged({ (value) in
if let match = rect.firstIndex(where: { $0.contains(value.location) }) {
colors[match] = Color.red
}
})
)
}
}
I'm currently learning SwiftUI and building a todo list app. On the ContentView screen I've got a NavigationView and a button that pops up an "add new task" textfield into the list. I suspect this is not the correct way to implement this but when the textfield shows up the background color doesn't persist. For the life of me I can't figure out how to set the background color. If I move the textfield outside the NavigationView I can set the background but when the NavigationView shifts to make space for the textfield I get a bunch of black screen flicker. Any thoughts on how I can set the background color on the textfield when added to the list or fix the screen flicker when I move it out? Appreciate the help.
import SwiftUI
import UIKit
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: ToDoItem.entity(), sortDescriptors: [NSSortDescriptor(key: "order", ascending: true)]) var listItems: FetchedResults<ToDoItem>
#State private var newToDoItem = ""
#State private var showNewTask = false
#State var isEditing = false
#State var showTaskView = false
#State var bottomState = CGSize.zero
#State var showFull = false
#State var deleteButton = false
//this removes the lines in the list view
init() {
// To remove only extra separators below the list:
UITableView.appearance().tableFooterView = UIView()
// To remove all separators including the actual ones:
UITableView.appearance().separatorStyle = .none
UIScrollView.appearance().backgroundColor = .clear
//UITableView.appearance().backgroundColor = .clear
}
var body: some View {
ZStack{
VStack{
TitleView()
NavigationView {
List {
if showNewTask {
HStack{
TextField("New task", text: self.$newToDoItem, onEditingChanged: { (changed) in
}) {
print("onCommit")
self.addTask(taskTitle: self.newToDoItem)
self.saveTasks()
self.showNewTask.toggle()
self.newToDoItem = ""
}
.font(Font.system(size: 18, weight: .bold))
.foregroundColor(Color("Text"))
Button(action: {
self.newToDoItem = ""
self.showNewTask.toggle()
}) {
Image(systemName: "xmark.circle").foregroundColor(Color("button"))
.font(Font.system(size: 18, weight: .bold))
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.background(Color("addNewTask"))
.cornerRadius(10.0)
}
ForEach(listItems, id: \.self) {item in
HStack {
Button(action: {
item.isComplete = true
self.saveTasks()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
self.deleteTaskTest(item: item)
}
}) {
if (item.isComplete) {
Image(systemName: "checkmark.circle")
.font(Font.system(size: 25, weight: .bold))
.foregroundColor(Color(#colorLiteral(red: 0.1616941956, green: 0.9244045403, blue: 0.1405039469, alpha: 1)))
.padding(.trailing, 4)
} else {
Image(systemName: "circle")
.font(Font.system(size: 25, weight: .bold))
.foregroundColor(Color("button"))
.padding(.trailing, 4)
}
}
.buttonStyle(PlainButtonStyle())
ToDoItemView(title: item.title, createdAt: "\(item.createdAt)")
.onTapGesture {
self.showTaskView.toggle()
}
.onLongPressGesture(minimumDuration: 0.1) {
self.isEditing.toggle()
print("this is a long press test")
}
}
.listRowBackground(Color("background"))
}
.onMove(perform: moveItem)
.onDelete(perform: deleteTask)
}
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
.navigationBarTitle(Text("ToDay"), displayMode: .large)
.navigationBarHidden(true)
.background(Color("background"))
}
//ADD A NEW TASK BUTTON
HStack {
Spacer()
Button(action: {
self.showNewTask.toggle()
}) {
Image(systemName: "plus")
.font(.system(size: 18, weight: .bold))
.frame(width: 36, height: 36)
.background(Color("button"))
.foregroundColor(.white)
.clipShape(Circle())
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
}
}
.padding()
}
.blur(radius: showTaskView ? 20 : 0)
.animation(.default)
.padding(.top, 30)
//BOTTOM CARD VIEW
TaskView()
.offset(x: 0, y: showTaskView ? 360 : 1000)
.offset(y: bottomState.height)
.animation(.timingCurve(0.2, 0.8, 0.2, 1, duration: 0.5))
.gesture(
DragGesture().onChanged { value in
self.bottomState = value.translation
if self.showFull {
self.bottomState.height += -300
}
if self.bottomState.height < -300 {
self.bottomState.height = -300
}
} .onEnded { value in
if self.bottomState.height > 50 {
self.showTaskView = false
}
if (self.bottomState.height < -100 && !self.showFull) || (self.bottomState.height < -250 && self.showFull){
self.bottomState.height = -300
self.showFull = true
} else {
self.bottomState = .zero
self.showFull = false
}
}
)
}
.background(Color("background").edgesIgnoringSafeArea(.all))
}
Finally got it to work. For whatever reason reworking the stacks fixed it.
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: ToDoItem.entity(), sortDescriptors: [NSSortDescriptor(key: "order", ascending: true)]) var listItems: FetchedResults<ToDoItem>
#State private var showCancelButton: Bool = false
#State private var newToDoItem = ""
#State private var showNewTask = false
#State var isEditing = false
#State var showTaskView = false
#State var bottomState = CGSize.zero
#State var showFull = false
#State var deleteButton = false
var itemName = ""
init() {
// To remove all separators including the actual ones:
UITableView.appearance().separatorStyle = .none
UITableView.appearance().backgroundColor = .clear
}
var body: some View {
ZStack {
VStack {
NavigationView {
VStack {
TitleView()
.padding(.top, 20)
.background(Color("background"))
// Enter new task view
if showNewTask {
HStack {
HStack {
TextField("New task", text: self.$newToDoItem, onEditingChanged: { (changed) in
}) {
self.addTask(taskTitle: self.newToDoItem)
self.saveTasks()
self.showNewTask.toggle()
self.newToDoItem = ""
}
.font(Font.system(size: 18, weight: .bold))
.foregroundColor(Color("Text"))
Button(action: {
self.newToDoItem = ""
self.showNewTask.toggle()
}) {
Image(systemName: "xmark.circle").foregroundColor(Color("button"))
.font(Font.system(size: 18, weight: .bold))
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.background(Color("addNewTask"))
.cornerRadius(10.0)
}
.background(Color("background"))
.padding(.horizontal)
}
List {
ForEach(listItems, id: \.self) {item in
HStack {
Button(action: {
item.isComplete = true
self.saveTasks()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
self.deleteTaskTest(item: item)
}
}) {
if (item.isComplete) {
Image(systemName: "checkmark.circle")
.font(Font.system(size: 25, weight: .bold))
.foregroundColor(Color(#colorLiteral(red: 0.1616941956, green: 0.9244045403, blue: 0.1405039469, alpha: 1)))
.padding(.trailing, 4)
} else {
Image(systemName: "circle")
.font(Font.system(size: 25, weight: .bold))
.foregroundColor(Color("button"))
.padding(.trailing, 4)
}
}
.buttonStyle(PlainButtonStyle())
ToDoItemView(title: item.title, createdAt: "\(item.createdAt)")
.onTapGesture {
//item.title = self.itemName
self.showTaskView.toggle()
}
.onLongPressGesture(minimumDuration: 0.1) {
self.isEditing.toggle()
print("this is a long press test")
}
}
.listRowBackground(Color("background"))
}
.onMove(perform: moveItem)
.onDelete(perform: deleteTask)
}
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
.navigationBarTitle(Text("ToDay"), displayMode: .large)
.navigationBarHidden(true)
.background(Color("background"))
}
.background(Color("background").edgesIgnoringSafeArea(.all))
}
HStack {
Spacer()
Button(action: {
//withAnimation(){
self.showNewTask.toggle()
//}
}) {
Image(systemName: "plus")
.font(.system(size: 18, weight: .bold))
.frame(width: 36, height: 36)
.background(Color("button"))
.foregroundColor(.white)
.clipShape(Circle())
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
}
}
.padding()
}
.blur(radius: showTaskView ? 20 : 0)
//BOTTOM CARD VIEW
TaskView()
.offset(x: 0, y: showTaskView ? 360 : 1000)
.offset(y: bottomState.height)
.animation(.timingCurve(0.2, 0.8, 0.2, 1, duration: 0.5))
.gesture(
DragGesture().onChanged { value in
self.bottomState = value.translation
if self.showFull {
self.bottomState.height += -300
}
if self.bottomState.height < -300 {
self.bottomState.height = -300
}
} .onEnded { value in
if self.bottomState.height > 50 {
self.showTaskView = false
}
if (self.bottomState.height < -100 && !self.showFull) || (self.bottomState.height < -250 && self.showFull){
self.bottomState.height = -300
self.showFull = true
} else {
self.bottomState = .zero
self.showFull = false
}
}
)
}
.animation(.default)
.background(Color("background").edgesIgnoringSafeArea(.all))
}
func moveItem(indexSet: IndexSet, destination: Int){
let source = indexSet.first!
if source < destination {
var startIndex = source + 1
let endIndex = destination - 1
var startOrder = listItems[source].order
while startIndex <= endIndex {
listItems[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
listItems[source].order = startOrder
} else if destination < source {
var startIndex = destination
let endIndex = source - 1
var startOrder = listItems[destination].order + 1
let newOrder = listItems[destination].order
while startIndex <= endIndex {
listItems[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
listItems[source].order = newOrder
}
saveTasks()
self.isEditing.toggle()
}
func deleteTask(indexSet: IndexSet){
let source = indexSet.first!
let listItem = listItems[source]
//self.deleteButton.toggle()
managedObjectContext.delete(listItem)
saveTasks()
}
func deleteTaskTest(item: ToDoItem){
managedObjectContext.delete(item)
saveTasks()
}
func addTask(taskTitle: String) {
let newTask = ToDoItem(context: managedObjectContext)
newTask.title = taskTitle
newTask.order = (listItems.last?.order ?? 0) + 1
newTask.createdAt = Date()
}
func saveTasks() {
do {
try managedObjectContext.save()
} catch {
print(error)
}
}