I followed this tutorial.. https://www.youtube.com/watch?v=fB5MzDD1PZI&list=WL&index=32&t=0s
and I am trying to add timer to it as automatically go to the next image. Any suggestion? Thanks.
This is my current code with some missing code and I don't know what to do next.
import SwiftUI
struct ContentView: View {
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State var Counter = 0
var body: some View {
ZStack {
VStack {
Text("HELLOWORLD")
VStack{
CarouselView(itemHeight: 400, views: [
AnyView(Text("1")),
AnyView(Text("2")),
AnyView(Text("3")),
AnyView(Text("4")),
AnyView(Text("5")),
AnyView(Text("6")),
]).onReceive(timer) { _ in
if self.Counter > 0 {
self.Counter -= 1
//missing some code to move index of carousel to i+1
}
}
}
Button(action: {
self.Counter = 10
}) {
Text("RUN")
}
}
}
}
}
struct CarouselView: View {
#GestureState private var dragState = DragState.inactive
#State var carouselLocation = 0
var itemHeight:CGFloat
var views:[AnyView]
private func onDragEnded(drag: DragGesture.Value) {
print("drag ended")
let dragThreshold:CGFloat = 200
if drag.predictedEndTranslation.width > dragThreshold || drag.translation.width > dragThreshold{
carouselLocation = carouselLocation - 1
} else if (drag.predictedEndTranslation.width) < (-1 * dragThreshold) || (drag.translation.width) < (-1 * dragThreshold)
{
carouselLocation = carouselLocation + 1
}
}
var body: some View {
ZStack{
VStack{
ZStack{
ForEach(0..<views.count){i in
VStack{
Spacer()
self.views[i]
//Text("\(i)")
.frame(width:300, height: self.getHeight(i))
.animation(.interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0))
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 3)
.opacity(self.getOpacity(i))
.animation(.interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0))
.offset(x: self.getOffset(i))
.animation(.interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0))
Spacer()
}
}
}.gesture(
DragGesture()
.updating($dragState) { drag, state, transaction in
state = .dragging(translation: drag.translation)
}
.onEnded(onDragEnded)
)
Spacer()
}
VStack{
Spacer()
Spacer().frame(height:itemHeight + 50)
Text("\(relativeLoc() + 1)/\(views.count)").padding()
Spacer()
}
}
}
func relativeLoc() -> Int{
return ((views.count * 10000) + carouselLocation) % views.count
}
func getHeight(_ i:Int) -> CGFloat{
if i == relativeLoc(){
return itemHeight
} else {
return itemHeight - 100
}
}
func getOpacity(_ i:Int) -> Double{
if i == relativeLoc()
|| i + 1 == relativeLoc()
|| i - 1 == relativeLoc()
|| i + 2 == relativeLoc()
|| i - 2 == relativeLoc()
|| (i + 1) - views.count == relativeLoc()
|| (i - 1) + views.count == relativeLoc()
|| (i + 2) - views.count == relativeLoc()
|| (i - 2) + views.count == relativeLoc()
{
return 1
} else {
return 0
}
}
func getOffset(_ i:Int) -> CGFloat{
//This sets up the central offset
if (i) == relativeLoc()
{
//Set offset of cental
return self.dragState.translation.width
}
//These set up the offset +/- 1
else if
(i) == relativeLoc() + 1
||
(relativeLoc() == views.count - 1 && i == 0)
{
//Set offset +1
return self.dragState.translation.width + (300 + 20)
}
else if
(i) == relativeLoc() - 1
||
(relativeLoc() == 0 && (i) == views.count - 1)
{
//Set offset -1
return self.dragState.translation.width - (300 + 20)
}
//These set up the offset +/- 2
else if
(i) == relativeLoc() + 2
||
(relativeLoc() == views.count-1 && i == 1)
||
(relativeLoc() == views.count-2 && i == 0)
{
return self.dragState.translation.width + (2*(300 + 20))
}
else if
(i) == relativeLoc() - 2
||
(relativeLoc() == 1 && i == views.count-1)
||
(relativeLoc() == 0 && i == views.count-2)
{
//Set offset -2
return self.dragState.translation.width - (2*(300 + 20))
}
//These set up the offset +/- 3
else if
(i) == relativeLoc() + 3
||
(relativeLoc() == views.count-1 && i == 2)
||
(relativeLoc() == views.count-2 && i == 1)
||
(relativeLoc() == views.count-3 && i == 0)
{
return self.dragState.translation.width + (3*(300 + 20))
}
else if
(i) == relativeLoc() - 3
||
(relativeLoc() == 2 && i == views.count-1)
||
(relativeLoc() == 1 && i == views.count-2)
||
(relativeLoc() == 0 && i == views.count-3)
{
//Set offset -2
return self.dragState.translation.width - (3*(300 + 20))
}
//This is the remainder
else {
return 10000
}
}
}
enum DragState {
case inactive
case dragging(translation: CGSize)
var translation: CGSize {
switch self {
case .inactive:
return .zero
case .dragging(let translation):
return translation
}
}
var isDragging: Bool {
switch self {
case .inactive:
return false
case .dragging:
return true
}
}
}
Ps. Sorry for misunderstand the rule. I am new for both StackOverFlow and SwiftUI. Thanks you.
check this out:
import SwiftUI
struct ContentView: View {
#State var location : Int = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State var Counter = 0
var body: some View {
ZStack {
VStack {
Text("HELLOWORLD")
VStack{
CarouselView(carouselLocation: self.$location, itemHeight: 400, views: [
AnyView(Text("1")),
AnyView(Text("2")),
AnyView(Text("3")),
AnyView(Text("4")),
AnyView(Text("5")),
AnyView(Text("6")),
]).onReceive(timer) { _ in
if self.Counter > 0 {
self.Counter -= 1
//missing some code to move index of carousel to i+1
}
}
}
Button(action: {
self.Counter = 10
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { (timer) in
self.location += 1
}
}) {
Text("RUN")
}
}
}
}
}
struct CarouselView: View {
#GestureState private var dragState = DragState.inactive
#Binding var carouselLocation : Int
var itemHeight:CGFloat
var views:[AnyView]
private func onDragEnded(drag: DragGesture.Value) {
print("drag ended")
let dragThreshold:CGFloat = 200
if drag.predictedEndTranslation.width > dragThreshold || drag.translation.width > dragThreshold{
carouselLocation = carouselLocation - 1
} else if (drag.predictedEndTranslation.width) < (-1 * dragThreshold) || (drag.translation.width) < (-1 * dragThreshold)
{
carouselLocation = carouselLocation + 1
}
}
var body: some View {
ZStack{
VStack{
ZStack{
ForEach(0..<views.count){i in
VStack{
Spacer()
self.views[i]
//Text("\(i)")
.frame(width:300, height: self.getHeight(i))
.animation(.interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0))
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 3)
.opacity(self.getOpacity(i))
.animation(.interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0))
.offset(x: self.getOffset(i))
.animation(.interpolatingSpring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0))
Spacer()
}
}
}.gesture(
DragGesture()
.updating($dragState) { drag, state, transaction in
state = .dragging(translation: drag.translation)
}
.onEnded(onDragEnded)
)
Spacer()
}
VStack{
Spacer()
Spacer().frame(height:itemHeight + 50)
Text("\(relativeLoc() + 1)/\(views.count)").padding()
Spacer()
}
}
}
func relativeLoc() -> Int{
return ((views.count * 10000) + carouselLocation) % views.count
}
func getHeight(_ i:Int) -> CGFloat{
if i == relativeLoc(){
return itemHeight
} else {
return itemHeight - 100
}
}
func getOpacity(_ i:Int) -> Double{
if i == relativeLoc()
|| i + 1 == relativeLoc()
|| i - 1 == relativeLoc()
|| i + 2 == relativeLoc()
|| i - 2 == relativeLoc()
|| (i + 1) - views.count == relativeLoc()
|| (i - 1) + views.count == relativeLoc()
|| (i + 2) - views.count == relativeLoc()
|| (i - 2) + views.count == relativeLoc()
{
return 1
} else {
return 0
}
}
func getOffset(_ i:Int) -> CGFloat{
//This sets up the central offset
if (i) == relativeLoc()
{
//Set offset of cental
return self.dragState.translation.width
}
//These set up the offset +/- 1
else if
(i) == relativeLoc() + 1
||
(relativeLoc() == views.count - 1 && i == 0)
{
//Set offset +1
return self.dragState.translation.width + (300 + 20)
}
else if
(i) == relativeLoc() - 1
||
(relativeLoc() == 0 && (i) == views.count - 1)
{
//Set offset -1
return self.dragState.translation.width - (300 + 20)
}
//These set up the offset +/- 2
else if
(i) == relativeLoc() + 2
||
(relativeLoc() == views.count-1 && i == 1)
||
(relativeLoc() == views.count-2 && i == 0)
{
return self.dragState.translation.width + (2*(300 + 20))
}
else if
(i) == relativeLoc() - 2
||
(relativeLoc() == 1 && i == views.count-1)
||
(relativeLoc() == 0 && i == views.count-2)
{
//Set offset -2
return self.dragState.translation.width - (2*(300 + 20))
}
//These set up the offset +/- 3
else if
(i) == relativeLoc() + 3
||
(relativeLoc() == views.count-1 && i == 2)
||
(relativeLoc() == views.count-2 && i == 1)
||
(relativeLoc() == views.count-3 && i == 0)
{
return self.dragState.translation.width + (3*(300 + 20))
}
else if
(i) == relativeLoc() - 3
||
(relativeLoc() == 2 && i == views.count-1)
||
(relativeLoc() == 1 && i == views.count-2)
||
(relativeLoc() == 0 && i == views.count-3)
{
//Set offset -2
return self.dragState.translation.width - (3*(300 + 20))
}
//This is the remainder
else {
return 10000
}
}
}
enum DragState {
case inactive
case dragging(translation: CGSize)
var translation: CGSize {
switch self {
case .inactive:
return .zero
case .dragging(let translation):
return translation
}
}
var isDragging: Bool {
switch self {
case .inactive:
return false
case .dragging:
return true
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Related
I am trying to create a tracking system. I want to show the views in each transition with my UI work on other pages. The "12 Ortak" part looks empty but the PersonListUIView() view should have been there:
struct FollowerUI: View {
#State var index = 1
#State var offset : CGFloat = UIScreen.main.bounds.width
var width = UIScreen.main.bounds.width
var body: some View {
VStack(spacing:0){
FollowerScreenUIView( index: self.$index, offset: self.$offset)
GeometryReader{ g in
HStack(spacing:0){
PersonListUIView()
.frame(width: g.frame(in: .global).width)
SortingListUIView()
.frame(width: g.frame(in: .global).width)
TopSortingUIView()
.frame(width: g.frame(in: .global).width)
}
.offset(x: self.offset)
.highPriorityGesture(DragGesture()
.onEnded({ (value) in
if value.translation.width > 50 {
print("right")
self.changeView(left: false)
}
if -value.translation.width > 50 {
print("left")
self.changeView(left: true)
}
}))
}
}
.edgesIgnoringSafeArea(.all)
}
func changeView(left: Bool ) {
if left{
if self.index != 3 {
self.index += 1
}
} else {
if self.index != 0 {
self.index -= 1
}
}
if self.index == 1 {
self.offset = self.width
}
else if self.index == 2 {
self.offset = 0
}
else {
self.offset = -self.width
}
}
}
struct FollowerScreenUIView: View {
var tabBarOption : [FollowingInfoResponse] = FollowingExampleData.followingExampleData
#Binding var index : Int
#Binding var offset: CGFloat
var width = UIScreen.main.bounds.width
var body: some View {
VStack(alignment: .leading, content:{
ForEach(tabBarOption){region in
HStack(spacing:24){
Button(action:{
self.index = 1
self.offset = self.width
}){
VStack(spacing: 2){
Text("\(String(region.commonCount)) Ortak")
.foregroundColor(self.index == 1 ? .black: Color.init(red: 0.733, green: 0.78, blue: 0.808).opacity(0.7))
Capsule()
.fill(self.index == 1 ? Color.black : Color.init(red: 0.733, green: 0.78, blue: 0.808).opacity(0.7))
.frame(height: 3)
}
}
Button(action:{
self.index = 2
self.offset = 0
}){
VStack(spacing: 2){
Text("\(String(region.followerCount)) Takip")
.foregroundColor(self.index == 2 ? .black: Color.init(red: 0.733, green: 0.78, blue: 0.808).opacity(0.7))
Capsule()
.fill(self.index == 2 ? Color.black : Color.init(red: 0.733, green: 0.78, blue: 0.808).opacity(0.7))
.frame(height: 3)
}
}
Button(action:{
self.index = 3
self.offset = -self.width
}){
VStack(spacing: 2){
Text("\(String(region.followingCount)) Takipçi")
.foregroundColor(self.index == 3 ? .black: Color.init(red: 0.733, green: 0.78, blue: 0.808).opacity(0.7))
Capsule()
.fill(self.index == 3 ? Color.black : Color.init(red: 0.733, green: 0.78, blue: 0.808).opacity(0.7))
.frame(height: 3)
}
}
}
.padding(.horizontal)
.padding(.bottom, 8)
.background(Color.white)
}
})
}
}
When the screen first opens, I want to appear as this 'view' -> 'PersonListUIView()'. But the "12 Partners" section appears to be empty. I have shared a picture for you to understand this better.
The Picture is here:
I'm getting the debug console error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Language name]: unrecognized selector sent to instance 0x11c6f8920'
but only when running app in iOS 16, iOS 15.7 does not crash.
Something in my code was acceptable pre iOS 16, and it's not acceptable in iOS 16.
The line the crash happens on is:
Button(language.name ?? "", action: {
Here is the relevant code:
import SwiftUI
import Foundation
import CoreData
struct CategoriesRevisionView: View {
#State private var selectedLanguage: String = ""
#State private var selectedLanguageSection: String = ""
#State var languagesWithSectionThingHomework: [Language] = []
#FetchRequest(entity: Language.entity(), sortDescriptors: []) var languages: FetchedResults<Language>
#FetchRequest(entity: LanguageSection.entity(), sortDescriptors: []) var languageSections: FetchedResults<LanguageSection>
#FetchRequest(entity: SectionThing.entity(), sortDescriptors: [NSSortDescriptor(key: "native", ascending: true)]) var sectionThings: FetchedResults<SectionThing>
#FetchRequest(entity: CommonWordThing.entity(), sortDescriptors: []) var commonWords: FetchedResults<CommonWordThing>
let tintColour = TintColour()
var revisionThings: [SectionThing] {
if self.selectedLanguageSection == "" {
return sectionThings.filter{ $0.setAsRevision == true && $0.languageSection?.language?.name == selectedLanguage }
} else {
return sectionThings.filter{ $0.setAsRevision == true && $0.languageSection?.name == selectedLanguageSection && $0.languageSection?.language?.name == selectedLanguage}
}
}
var languagesToAddHomework: [Language] {
var languagesWithoutHomework: [Language] = []
for languageSection in languageSections {
if languageSection.setAsRevision == false {
if !languagesWithoutHomework.contains(languageSection.language ?? Language()) {
languagesWithoutHomework.append(languageSection.language ?? Language())
}
}
}
return languagesWithoutHomework
}
var languagesToRemoveHomework: [Language] {
var languagesWithHomework: [Language] = []
for languageSection in languageSections {
if languageSection.setAsRevision == true {
if !languagesWithHomework.contains(languageSection.language ?? Language()) {
languagesWithHomework.append(languageSection.language ?? Language())
}
}
}
return languagesWithHomework
}
var languageSectionsToAddHomework: [LanguageSection] {
var languageSectionsWithoutHomework: [LanguageSection] = []
for languageSection in languageSections {
if languageSection.setAsRevision == false {
if !languageSectionsWithoutHomework.contains(languageSection) {
languageSectionsWithoutHomework.append(languageSection)
}
}
}
return languageSectionsWithoutHomework
}
var languageSectionsToRemoveHomework: [LanguageSection] {
var languageSectionsWithHomework: [LanguageSection] = []
for languageSection in languageSections {
if languageSection.setAsRevision == true {
if !languageSectionsWithHomework.contains(languageSection) {
languageSectionsWithHomework.append(languageSection)
}
}
}
return languageSectionsWithHomework
}
var languageCategories: [LanguageSection] {
return languageSections.filter { $0.setAsRevision == true }
}
var body: some View {
List {
ForEach(revisionThings ?? [], id: \.self) { revisionThing in
ThingRowView(thing: revisionThing, commonWords: Array(commonWords), resizedImage: UIImage())
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button {
setRevision.unsetFilteredHomeworkThingRevision(sectionThings: [revisionThing], languageSections: Array(languageSections), commonWordThings: Array(commonWords))
computeLanguagesWithSectionHomework()
selectLanguageAndSection()
PersistenceController.shared.saveDB()
dataSetIsEmpty()
} label: {
Image("removeHomework")
}
.tint(tintColour.SetRevisionSectionThing(sectionThing: revisionThing))
}
}
}
.listStyle(InsetGroupedListStyle())
.background(Color.init(.systemGroupedBackground))
.navigationBarTitle("Revision", displayMode: .inline).opacity(0.8)
.toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) {
Menu {
ForEach(languagesWithSectionThingHomework, id: \.self) { language in
Button("\(language.name ?? "")", action: {
self.selectedLanguage = language.name ?? ""
self.selectedLanguageSection = ""
})
Menu("Categories") {
ForEach(languageCategories, id: \.self) { languageSection in
if languageSection.language == language {
Button("\(languageSection.name ?? "")", action: {
self.selectedLanguage = language.name ?? ""
self.selectedLanguageSection = languageSection.name ?? ""
})
}
}
}
Divider()
}
} label: {
if languageSections.filter{ $0.setAsRevision == true }.count <= 1 {
Label("Choose Homework Language", image: "menu")
} else {
Label("Choose Homework Language", image: "menu").foregroundColor(Color.systemBlue)
}
}
.disabled(languageSections.filter{ $0.setAsRevision == true }.count <= 1)
Menu {
ForEach(languagesToAddHomework, id: \.self) { language in
Button(language.name ?? "", action: {
setRevision.setSectionThingRevision(sectionThings: sectionThings.filter { $0.languageSection?.language == language }, commonWordThings: Array(commonWords), languageSections: Array(languageSections))
computeLanguagesWithSectionHomework()
self.selectedLanguage = language.name ?? ""
self.selectedLanguageSection = ""
})
Menu("Categories") {
ForEach(languageSectionsToAddHomework.filter { $0.language == language }, id: \.self) { languageSection in
Button(languageSection.name ?? "", action: {
setRevision.setSectionThingRevision(sectionThings: sectionThings.filter { $0.languageSection?.language == language && $0.languageSection == languageSection }, commonWordThings: Array(commonWords), languageSections: Array(languageSections))
computeLanguagesWithSectionHomework()
self.selectedLanguage = language.name ?? ""
self.selectedLanguageSection = languageSection.name ?? ""
})
}
}
Divider()
}
} label: {
if sectionThings.filter{ $0.setAsRevision == true }.count == sectionThings.count {
Image("revisionAdd")
} else {
Image("revisionAdd").foregroundColor(.green)
}
}
.disabled(sectionThings.filter{ $0.setAsRevision == true }.count == sectionThings.count)
Menu {
ForEach(languagesToRemoveHomework, id: \.self) { language in
Button(language.name ?? "", action: {
setRevision.unsetFilteredHomeworkThingRevision(sectionThings: sectionThings.filter { $0.languageSection?.language == language }, languageSections: Array(languageSections), commonWordThings: Array(commonWords))
computeLanguagesWithSectionHomework()
selectLanguageAndSection()
dataSetIsEmpty()
})
Menu("Categories") {
ForEach(languageSectionsToRemoveHomework.filter { $0.language == language }, id: \.self) { languageSection in
Button(languageSection.name ?? "", action: {
setRevision.unsetFilteredHomeworkThingRevision(sectionThings: languageSection.sectionThing?.allObjects as? [SectionThing] ?? [], languageSections: Array(languageSections), commonWordThings: Array(commonWords))
computeLanguagesWithSectionHomework()
selectLanguageAndSection()
dataSetIsEmpty()
})
}
}
Divider()
}
} label: {
if sectionThings.filter{ $0.setAsRevision == true }.count == 0 {
Image("revisionRemove")
} else {
Image("revisionRemove").foregroundColor(.yellow)
}
}
.disabled(sectionThings.filter{ $0.setAsRevision == true }.count == 0)
}
}
.onAppear(perform: initialize)
}
func initialize() {
outerloop: for language in languages {
if let languageSections = language.languageSection?.allObjects as? [LanguageSection] {
for languageSection in languageSections {
if let languageSectionThings = languageSection.sectionThing?.allObjects as? [SectionThing] {
if languageSectionThings.filter({ $0.setAsRevision == true }).count > 0 {
self.selectedLanguage = language.name ?? ""
self.selectedLanguageSection = languageSection.name ?? ""
break outerloop
}
}
}
}
}
dataSetIsEmpty()
computeLanguagesWithSectionHomework()
}
func computeLanguagesWithSectionHomework() {
var sectionThingLanguages: [Language] = []
for language in languages {
if languageSections.filter({ $0.language == language && $0.setAsRevision == true }).count > 0 {
if !sectionThingLanguages.contains(language) {
sectionThingLanguages.append(language)
}
}
}
self.languagesWithSectionThingHomework = sectionThingLanguages
}
func selectLanguageAndSection() {
if revisionThings?.count == 0 {
if languageSections.filter({ $0.sectionThing?.allObjects.count ?? 0 > 0 && $0.setAsRevision == true }).count > 0 {
selectedLanguageSection = languageSections.filter{ $0.sectionThing?.allObjects.count ?? 0 > 0 && $0.setAsRevision == true }[0].name ?? ""
selectedLanguage = languageSections.filter{ $0.sectionThing?.allObjects.count ?? 0 > 0 && $0.setAsRevision == true }[0].language?.name ?? ""
}
}
}
}
I'm getting into building Apple Watch apps.
What I'm currently working on will require me to make use of detecting swipes in the four main directions (UP, DOWN, LEFT and RIGHT)
The problem is I have no idea how to detect this. I've been looking around and I'm reaching dead ends.
What can I do to my view below to just print swiped up when the user swipes UP on the view?
struct MyView: View {
var body: some View {
Text("Hello, World!")
}
}
Thanks.
You could use DragGesture
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onEnded({ value in
if value.translation.width < 0 {
// left
}
if value.translation.width > 0 {
// right
}
if value.translation.height < 0 {
// up
}
if value.translation.height > 0 {
// down
}
}))
With the other solutions being a bit inconsistent on a physical device, I decided to come up with another one that seems to be much more consistent across different screen sizes as there are no hardcoded values except for the minimumDistance.
.gesture(DragGesture(minimumDistance: 20, coordinateSpace: .global)
.onEnded { value in
let horizontalAmount = value.translation.width
let verticalAmount = value.translation.height
if abs(horizontalAmount) > abs(verticalAmount) {
print(horizontalAmount < 0 ? "left swipe" : "right swipe")
} else {
print(verticalAmount < 0 ? "up swipe" : "down swipe")
}
})
If you want one that is more "forgiving" to the directionality of the swipe, you can use a few more conditionals to help even it out:
EDIT: did some more testing, apparently the values for the second conditional add some confusion, so I adjusted them to remove said confusion and make the gesture bulletproof (drags to the corners will now come up with "no clue" instead of one of the gestures)...
let detectDirectionalDrags = DragGesture(minimumDistance: 3.0, coordinateSpace: .local)
.onEnded { value in
print(value.translation)
if value.translation.width < 0 && value.translation.height > -30 && value.translation.height < 30 {
print("left swipe")
}
else if value.translation.width > 0 && value.translation.height > -30 && value.translation.height < 30 {
print("right swipe")
}
else if value.translation.height < 0 && value.translation.width < 100 && value.translation.width > -100 {
print("up swipe")
}
else if value.translation.height > 0 && value.translation.width < 100 && value.translation.width > -100 {
print("down swipe")
}
else {
print("no clue")
}
Based on Benjamin's answer this is a swiftier way to handle the cases
.gesture(DragGesture(minimumDistance: 3.0, coordinateSpace: .local)
.onEnded { value in
print(value.translation)
switch(value.translation.width, value.translation.height) {
case (...0, -30...30): print("left swipe")
case (0..., -30...30): print("right swipe")
case (-100...100, ...0): print("up swipe")
case (-100...100, 0...): print("down swipe")
default: print("no clue")
}
}
)
Create an extension:
extension View {
func swipe(
up: #escaping (() -> Void) = {},
down: #escaping (() -> Void) = {},
left: #escaping (() -> Void) = {},
right: #escaping (() -> Void) = {}
) -> some View {
return self.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onEnded({ value in
if value.translation.width < 0 { left() }
if value.translation.width > 0 { right() }
if value.translation.height < 0 { up() }
if value.translation.height > 0 { down() }
}))
}
}
Then:
Image() // or any View
.swipe(
up: {
// action for up
},
right: {
// action for right
})
Notice that each direction is an optional parameter
I would create a modifier for simplicity. Usage will look like that:
yourView
.onSwiped(.down) {
// Action for down swipe
}
OR
yourView
.onSwiped { direction in
// React to detected swipe direction
}
You can also use trigger parameter in order to configure receiving updates: continuously or only when the gesture ends.
Here's the full code:
struct SwipeModifier: ViewModifier {
enum Directions: Int {
case up, down, left, right
}
enum Trigger {
case onChanged, onEnded
}
var trigger: Trigger
var handler: ((Directions) -> Void)?
func body(content: Content) -> some View {
content.gesture(
DragGesture(
minimumDistance: 24,
coordinateSpace: .local
)
.onChanged {
if trigger == .onChanged {
handle($0)
}
}.onEnded {
if trigger == .onEnded {
handle($0)
}
}
)
}
private func handle(_ value: _ChangedGesture<DragGesture>.Value) {
let hDelta = value.translation.width
let vDelta = value.translation.height
if abs(hDelta) > abs(vDelta) {
handler?(hDelta < 0 ? .left : .right)
} else {
handler?(vDelta < 0 ? .up : .down)
}
}
}
extension View {
func onSwiped(
trigger: SwipeModifier.Trigger = .onChanged,
action: #escaping (SwipeModifier.Directions) -> Void
) -> some View {
let swipeModifier = SwipeModifier(trigger: trigger) {
action($0)
}
return self.modifier(swipeModifier)
}
func onSwiped(
_ direction: SwipeModifier.Directions,
trigger: SwipeModifier.Trigger = .onChanged,
action: #escaping () -> Void
) -> some View {
let swipeModifier = SwipeModifier(trigger: trigger) {
if direction == $0 {
action()
}
}
return self.modifier(swipeModifier)
}
}
This is much more responsive:
.gesture(DragGesture(minimumDistance: 3.0, coordinateSpace: .local)
.onEnded { value in
let direction = atan2(value.translation.width, value.translation.height)
switch direction {
case (-Double.pi/4..<Double.pi/4): self.playMove(.down)
case (Double.pi/4..<Double.pi*3/4): self.playMove(.right)
case (Double.pi*3/4...Double.pi), (-Double.pi..<(-Double.pi*3/4)):
self.playMove(.up)
case (-Double.pi*3/4..<(-Double.pi/4)): self.playMove(.left)
default:
print("unknown)")
}
}
Little bit late to this, but here's another implementation which uses OptionSet to make its use a bit more like various other SwiftUI components -
struct Swipe: OptionSet, Equatable {
init(rawValue: Int) {
self.rawValue = rawValue
}
let rawValue: Int
fileprivate var swiped: ((DragGesture.Value, Double) -> Bool) = { _, _ in false } // prevents a crash if someone creates a swipe directly using the init
private static let sensitivityFactor: Double = 400 // a fairly arbitrary figure which gives a reasonable response
static var left: Swipe {
var swipe = Swipe(rawValue: 1 << 0)
swipe.swiped = { value, sensitivity in
value.translation.width < 0 && value.predictedEndTranslation.width < sensitivity * sensitivityFactor
}
return swipe
}
static var right: Swipe {
var swipe = Swipe(rawValue: 1 << 1)
swipe.swiped = { value, sensitivity in
value.translation.width > 0 && value.predictedEndTranslation.width > sensitivity * sensitivityFactor
}
return swipe
}
static var up: Swipe {
var swipe = Swipe(rawValue: 1 << 2)
swipe.swiped = { value, sensitivity in
value.translation.height < 0 && value.predictedEndTranslation.height < sensitivity * sensitivityFactor
}
return swipe
}
static var down: Swipe {
var swipe = Swipe(rawValue: 1 << 3)
swipe.swiped = { value, sensitivity in
value.translation.height > 0 && value.predictedEndTranslation.height > sensitivity * sensitivityFactor
}
return swipe
}
static var all: Swipe {
[.left, .right, .up, .down]
}
private static var allCases: [Swipe] = [.left, .right, .up, .down]
fileprivate var array: [Swipe] {
Swipe.allCases.filter { self.contains($0) }
}
}
extension View {
func swipe(_ swipe: Swipe, sensitivity: Double = 1, action: #escaping (Swipe) -> ()) -> some View {
return gesture(DragGesture(minimumDistance: 30, coordinateSpace: .local)
.onEnded { value in
swipe.array.forEach { swipe in
if swipe.swiped(value, sensitivity) {
action(swipe)
}
}
}
)
}
}
In a SwiftUI view -
HStack {
// content
}
.swipe([.left, .right]) { swipe in // could also be swipe(.left) or swipe(.all), etc
doSomething(with: swipe)
}
Obviously the logic for detecting swipes is a bit basic, but that's easy enough to tailor to your requirements.
I cannot return DragGesture from computed property like below
'_EndedGesture<_ChangedGesture<DragGesture>>' to return type 'DragGesture'
var dimDrag : DragGesture {
DragGesture()
.onChanged({
print("Dim drag")
if $0.translation.width > 0 {
self.model.menuOffset = max(min($0.translation.width, UIScreen.main.bounds.width*0.7), 0.0)
} else {
self.model.menuOffset = max(min(UIScreen.main.bounds.width*0.7 + $0.translation.width, UIScreen.main.bounds.width*0.7), 0.0)
}
})
.onEnded({
if $0.translation.width < -100 {
withAnimation {
self.model.isMenuOpen = true
self.model.menuOffset = 0.0
}
} else if $0.translation.width > 100 {
withAnimation {
self.model.isMenuOpen = false
self.model.menuOffset = UIScreen.main.bounds.width*0.7
}
}
})
}
The following works for me:
let myDrag = {
DragGesture()
.onChanged({ value in
})
.onEnded { value in
}
}()
I'd like SwiftUI DragGesture to start only when the gesture is in a specific direction (horizontal/vertical). Is it possible?
Yes, it is, by applying one of the two components (either horizontal or vertical) of the gesture translation to the view offset.
Here's such behaviour implemented as a ViewModifier.
struct DraggableModifier : ViewModifier {
enum Direction {
case vertical
case horizontal
}
let direction: Direction
#State private var draggedOffset: CGSize = .zero
func body(content: Content) -> some View {
content
.offset(
CGSize(width: direction == .vertical ? 0 : draggedOffset.width,
height: direction == .horizontal ? 0 : draggedOffset.height)
)
.gesture(
DragGesture()
.onChanged { value in
self.draggedOffset = value.translation
}
.onEnded { value in
self.draggedOffset = .zero
}
)
}
}
Demo:
struct ContentView: View {
var body: some View {
VStack {
Spacer(minLength: 100)
HStack {
Rectangle()
.foregroundColor(.green)
.frame(width: 100, height: 100)
.modifier(DraggableModifier(direction: .vertical))
Text("Vertical")
}
Spacer(minLength: 100)
Rectangle()
.foregroundColor(.red)
.frame(width: 100, height: 100)
.modifier(DraggableModifier(direction: .horizontal))
Text(verbatim: "Horizontal")
Spacer(minLength: 100)
}
}
}
Result:
Rather old question but didn't find the answer elsewhere.
You can easily check wether it was a left- or a right-swipe, because Gestures return their end state, which contains the starting and final position.
Text("Hello")
.gesture(
DragGesture(minimumDistance: 100)
.onEnded { endedGesture in
if (endedGesture.location.x - endedGesture.startLocation.x) > 0 {
print("Right")
} else {
print("Left")
}
}
This will only move the view if the gesture moves move horizontally than vertically. The isDragging boolean makes sure after the first condition is met, the view will move vertically too. It's not necessary, but it helps for smoother dragging.
#State var dragOffset: CGSize = .zero
#State var isDragging = false
.offset(x: offset.width, y: offset.height)
.gesture(DragGesture()
.onChanged { if abs($0.translation.width) > abs($0.translation.height) || isDragging {
self.offset = $0.translation
self.isDragging = true
}
}
.onEnded {
isDragging = false
})
An example in which, at the start of the movement, a vertical or horizontal movement is determined after which the object moves only in this direction
struct Test: View {
#State private var offsetX = CGFloat.zero
#State private var offsetY = CGFloat.zero
var body: some View {
ZStack {
Divider()
Divider().rotationEffect(.degrees(90))
Divider().rotationEffect(.degrees(45))
Divider().rotationEffect(.degrees(-45))
VStack {
Spacer()
Text("offsetX: \(offsetX) offsetY: \(offsetY)")
.foregroundColor(.black.opacity(0.5))
.frame(maxWidth: .infinity)
.padding(.bottom, 18.0)
}
Rectangle()
.fill(.green)
.frame(width: 300.0, height: 200.0)
.cornerRadius(13.0)
.offset(x: offsetX, y: offsetY)
.gesture(drag)
}
}
#State private var horizontal = true
#State private var vertical = true
var drag: some Gesture {
DragGesture()
.onChanged { dg in
var x = dg.translation.width
var y = dg.translation.height
if ((x < 0 && y < 0 && x < y) || (x < 0 && y > 0 && -x > y) || (x > 0 && y < 0 && x > -y) || (x > 0 && y > 0 && x > y)) && self.horizontal && self.vertical {
self.horizontal = true
self.vertical = false
} else if vertical && horizontal {
self.horizontal = false
self.vertical = true
}
if self.horizontal {
self.offsetX = x
} else {
self.offsetY = y
}
}
.onEnded { _ in
withAnimation {
self.offsetX = .zero
self.offsetY = .zero
}
self.horizontal = true
self.vertical = true
}
}
}
Demo: https://i.stack.imgur.com/Vdbkx.gif