I have an accounting app with a screen that displays two views. One view displays Currently In Use Categories and another that displays Available Categories. The user may select which categories to use and remove other not needed. Tapping on a category in either box immediately moves the category to the other category box. These programmed categories may not be deleted.
In addition, the user may create their own optional categories. The trouble I'm having is deleting the optional categories. (The Add New Category works fine.) I know how to normally implement view delete using List and ForEach but this seems somewhat different. If I add onDelete at the bottom of the ForEach loop and change the VStack to a List I don't see any entries in the list. Changing the List to a VStack displays the categories in the list but doesn't let me delete any categories. Another possible problem is dragging a category to the left for deletion may also trigger the switch view button.
I am filtering the data because I was getting gaps in the lists for categories currently in the other view.
The code below is the logic for displaying the Currently In Use box.
struct GetCurrentCats: View {
var g: GeometryProxy
#State private var showStatus: Bool = false
#Binding var presentAlert: Bool
#Binding var presentDetail: String
#EnvironmentObject var categories: Categories
var body: some View {
//VStack (alignment: .leading, spacing: 6) {
List {
ForEach(categories.filteredInUse, id: \.id) { item in
Button(action: {
if item.catTotal == 0.0 { // must have a zero balance to remove
withAnimation {
showStatus = false
// Change the state of category
categories.setCatShow(forId: item.id)
}
} else {
presentAlert = true
presentDetail = item.catName
}
}){
HStack {
Image(systemName: item.catPix)
.resizable()
.foregroundColor(Color(.systemOrange))
.frame(width: 25, height: 25)
.frame(width: UIDevice.current.userInterfaceIdiom == .phone ? g.size.width * 0.40 : g.size.width * 0.20)
Text(item.catName)
.padding(.horizontal, 8)
.background(item.catTotal != 0.0 ? Color(.systemRed) : Color(.systemBlue))
.foregroundColor(Color.white)
.cornerRadius(8)
}
}
}.onDelete(perform: deleteCurCat)
}.frame(maxWidth: .infinity, alignment: .leading)
}
}
func deleteCurCat(indexSet: IndexSet) {
categories.catItem.remove(atOffsets: indexSet)
}
}
Here is class Categories:
// working categories
struct CatModel: Codable, Identifiable, Hashable {
var id = UUID()
var catName: String // category name
var catTotal: Double // category total
var catBudget: Double // category budget
var catPix: String // sf symbol
var catShow: Bool
/// Provides a new instance, toggling `catShow`.
var toggleCatShow: CatModel {
// Note: by creating a new instance, the new id will be different
CatModel(catName: catName, catTotal: catTotal, catBudget: catBudget, catPix: catPix, catShow: !catShow)
}
}
class Categories: ObservableObject {
#Published var filteredInUse: [CatModel] = []
#Published var filteredNotInUse: [CatModel] = []
#Published var catItem: [CatModel] {
didSet {
self.save()
}
}
init() {
// read in category data
if let catItem = UserDefaults.standard.data(forKey: StorageKeys.workCat.rawValue) {
if let decoded = try? JSONDecoder().decode([CatModel].self, from: catItem) {
self.catItem = decoded
self.updateStatus()
return
}
}
catItem = []
// catShow: 1 = category available for use; 2 = category currently being used
let item0 = CatModel(catName: "Lodging", catTotal: 0.0, catBudget: 0, catPix: "bed.double.fill", catShow: false)
self.catItem.append(item0)
let item1 = CatModel(catName: "Food", catTotal: 0.0, catBudget: 0, catPix: "cart", catShow: false)
self.catItem.append(item1)
let item2 = CatModel(catName: "Airplane", catTotal: 0.0, catBudget: 0, catPix: "airplane", catShow: false)
self.catItem.append(item2)
let item3 = CatModel(catName: "Train", catTotal: 0.0, catBudget: 0, catPix: "tram", catShow: false)
self.catItem.append(item3)
let item4 = CatModel(catName: "Bus", catTotal: 0.0, catBudget: 0, catPix: "bus.fill", catShow: false)
self.catItem.append(item4)
let item5 = CatModel(catName: "Ferry", catTotal: 0.0, catBudget: 0, catPix: "ferry", catShow: false) // "helm"
self.catItem.append(item5)
let item6 = CatModel(catName: "Local Transit", catTotal: 0.0, catBudget: 0, catPix: "textbox", catShow: false)
self.catItem.append(item6)
let item7 = CatModel(catName: "Sightseeing ", catTotal: 0.0, catBudget: 0, catPix: "photo", catShow: false)
self.catItem.append(item7)
let item8 = CatModel(catName: "Entertainment", catTotal: 0.0, catBudget: 0, catPix: "music.mic", catShow: false)
self.catItem.append(item8)
let item9 = CatModel(catName: "Souvenirs", catTotal: 0.0, catBudget: 0, catPix: "gift", catShow: false)
self.catItem.append(item9)
let item10 = CatModel(catName: "Laundry", catTotal: 0.0, catBudget: 0, catPix: "scribble", catShow: false)
self.catItem.append(item10)
let item11 = CatModel(catName: "Rental Car", catTotal: 0.0, catBudget: 0, catPix: "car", catShow: false)
self.catItem.append(item11)
let item12 = CatModel(catName: "Fuel", catTotal: 0.0, catBudget: 0, catPix: "fuelpump", catShow: false) // gauge
self.catItem.append(item12)
let item13 = CatModel(catName: "Parking", catTotal: 0.0, catBudget: 0, catPix: "parkingsign.circle", catShow: false)
self.catItem.append(item13)
self.updateStatus()
}
func updateStatus() {
filteredInUse = catItem.filter({ (user) -> Bool in
return user.catShow == true
})
filteredNotInUse = catItem.filter({ (user) -> Bool in
return user.catShow == false
})
}
// Replaces an instance of `CatModel` by changing its `catShow` value.
func setCatShow(forId id: UUID) {
let index = catItem.firstIndex { $0.id == id }
if let index = index {
catItem.replaceSubrange(index...index, with: [catItem[index].toggleCatShow])
}
// Refresh the filtered arrays
updateStatus()
}
add new optional category (8 max)
func addNewCatetory(catName: String) -> () {
let item = CatModel(catName: catName, catTotal: 0.0, catBudget: 0, catPix: "person.2", catShow: true)
catItem.append(item)
}
}
Here is the upper piece of my Update Categories logic. If I run the example code with GetCurrentCats(), it displays the programmed categories. If I include this upper portion of the logic that sets up the two boxes and text above and below each box then GetCurrentCats doesn't display any programmed categories but GetAvailableCats() does display its categories. It appears to be something with the onDelete in GetCurrentCats that it doesn't like.
struct ContentView: View {
#EnvironmentObject var categories: Categories
#State private var presentAlert = false
#State private var presentDetail: String = ""
var body: some View {
GeometryReader { g in
VStack (alignment: .center) {
Text("Current Categories")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top, 6)
.padding(.bottom, -3)
ScrollView {
GetCurrentCats(g: g, presentAlert: $presentAlert, presentDetail: $presentDetail)
} .frame(width: g.size.width * 0.80, height: g.size.height * 0.35)
.border(Color(.systemBlue), width: 4)
Text("Tap category to remove from system.")
.font(.footnote)
Text("Red categories have a nonzero balance.")
.font(.footnote)
Text("Categories must have a zero balance to remove.")
.font(.footnote)
.padding(.bottom, g.size.height * 0.04)
Text("Available Categories")
.font(.headline)
.padding(.top, g.size.height * 0.05)
.padding(.bottom, -3)
ScrollView {
GetAvailableCats(g: g)
} .frame(width: g.size.width * 0.80, height: g.size.height * 0.35).border(Color(.systemBlue), width: 4)
Text("Tap category to add to system.")
.font(.footnote)
}.alert(
"Unable to remove category.", isPresented: $presentAlert,
presenting: presentDetail
) { detail in
Button("OK") {}
} message: { detail in
Text("\(presentDetail) category has a nonzero balance.")
}
.navigationBarTitle("Setup Categories", displayMode: .inline)
.navigationViewStyle(StackNavigationViewStyle())
/* .navigationBarItems(trailing: NavigationLink (destination: AddCatView()) {
Image(systemName: "plus")
.resizable()
.frame(width: 18, height: 18)
}) */
}
}
}
you should not be using duplicate, triplicate arrays to store CatModels of different
catShow values. Use only one array catItem and filter the array according to your needs.
Such as:
filteredInUse equivalent, use categories.catItem.filter{ $0.catShow.wrappedValue } and
filteredNotInUse equivalent, use categories.catItem.filter{ !$0.catShow.wrappedValue } in the ForEach loop.
I've updated your code using this approach.
// working categories
struct CatModel: Codable, Identifiable, Hashable {
var id = UUID()
var catName: String // category name
var catTotal: Double // category total
var catBudget: Double // category budget
var catPix: String // sf symbol
var catShow: Bool
}
class Categories: ObservableObject {
// the array of CatModel
#Published var catItem: [CatModel] {
didSet {
// self.save()
}
}
init() {
// you should not use this to store the array of CatModels.
// UserDefaults is meant to be used only for small amount of data.
// read in category data
// if let catItem = UserDefaults.standard.data(forKey: StorageKeys.workCat.rawValue) {
// if let decoded = try? JSONDecoder().decode([CatModel].self, from: catItem) {
// self.catItem = decoded
// return
// }
// }
catItem = []
let item0 = CatModel(catName: "Lodging", catTotal: 0.0, catBudget: 0, catPix: "bed.double.fill", catShow: true)
self.catItem.append(item0)
let item1 = CatModel(catName: "Food", catTotal: 0.0, catBudget: 0, catPix: "cart", catShow: false)
self.catItem.append(item1)
let item2 = CatModel(catName: "Airplane", catTotal: 0.0, catBudget: 0, catPix: "airplane", catShow: true)
self.catItem.append(item2)
let item3 = CatModel(catName: "Train", catTotal: 0.0, catBudget: 0, catPix: "tram", catShow: false)
self.catItem.append(item3)
let item4 = CatModel(catName: "Bus", catTotal: 0.0, catBudget: 0, catPix: "bus.fill", catShow: true)
self.catItem.append(item4)
let item5 = CatModel(catName: "Ferry", catTotal: 0.0, catBudget: 0, catPix: "ferry", catShow: false) // "helm"
self.catItem.append(item5)
let item6 = CatModel(catName: "Local Transit", catTotal: 0.0, catBudget: 0, catPix: "textbox", catShow: true)
self.catItem.append(item6)
let item7 = CatModel(catName: "Sightseeing ", catTotal: 0.0, catBudget: 0, catPix: "photo", catShow: false)
self.catItem.append(item7)
let item8 = CatModel(catName: "Entertainment", catTotal: 0.0, catBudget: 0, catPix: "music.mic", catShow: true)
self.catItem.append(item8)
let item9 = CatModel(catName: "Souvenirs", catTotal: 0.0, catBudget: 0, catPix: "gift", catShow: false)
self.catItem.append(item9)
let item10 = CatModel(catName: "Laundry", catTotal: 0.0, catBudget: 0, catPix: "scribble", catShow: true)
self.catItem.append(item10)
let item11 = CatModel(catName: "Rental Car", catTotal: 0.0, catBudget: 0, catPix: "car", catShow: false)
self.catItem.append(item11)
let item12 = CatModel(catName: "Fuel", catTotal: 0.0, catBudget: 0, catPix: "fuelpump", catShow: true) // gauge
self.catItem.append(item12)
let item13 = CatModel(catName: "Parking", catTotal: 0.0, catBudget: 0, catPix: "parkingsign.circle", catShow: false)
self.catItem.append(item13)
}
// add new optional category (8 max)
func addNewCatetory(catName: String) {
catItem.append(CatModel(catName: catName, catTotal: 0.0, catBudget: 0, catPix: "person.2", catShow: true))
}
}
// for testing
struct ContentView: View {
#StateObject var categories = Categories()
#State var presentAlert = false
#State var presentDetail: String = ""
var body: some View {
GeometryReader { geom in
GetCurrentCats(g: geom, presentAlert: $presentAlert, presentDetail: $presentDetail)
.environmentObject(categories)
}
}
}
struct GetCurrentCats: View {
var g: GeometryProxy
#Binding var presentAlert: Bool
#Binding var presentDetail: String
#EnvironmentObject var categories: Categories
var body: some View {
VStack (alignment: .leading, spacing: 6) {
List {
// catShow=true , filteredInUse equivalent list
ForEach($categories.catItem.filter{ $0.catShow.wrappedValue }, id: \.id) { $item in
Button(action: {
if item.catTotal == 0.0 { // must have a zero balance to remove
withAnimation {
// Change the state of category
item.catShow = true
}
} else {
presentAlert = true
presentDetail = item.catName
}
}){
HStack {
Image(systemName: item.catPix)
.resizable()
.foregroundColor(Color(.systemOrange))
.frame(width: 25, height: 25)
.frame(width: UIDevice.current.userInterfaceIdiom == .phone ? g.size.width * 0.40 : g.size.width * 0.20)
Text(item.catName)
.padding(.horizontal, 8)
.background(item.catTotal != 0.0 ? Color(.systemRed) : Color(.systemBlue))
.foregroundColor(Color.white)
.cornerRadius(8)
}
}
}.onDelete(perform: deleteCurCat)
}.frame(maxWidth: .infinity, alignment: .leading)
}
}
func deleteCurCat(indexSet: IndexSet) {
if let index = indexSet.first {
// you have to filter exactly like in the ForEach
let cats = categories.catItem.filter({ $0.catShow })
if index < cats.count {
if let ndx = categories.catItem.firstIndex(where: { $0.id == cats[index].id }) {
// categories.catItem.remove(at: ndx) // if you want to totally remove the catItem
// to switch/transfer category, just toggle the catShow
categories.catItem[ndx].catShow.toggle()
}
}
}
}
}
EDIT-1: to show the list of categories:
Remove the ScrollView (recommended solution), or set the frame of GetCurrentCats, such as:
ScrollView {
GetCurrentCats(g: g, presentAlert: $presentAlert, presentDetail: $presentDetail)
.frame(width: g.size.width * 0.80, height: g.size.height * 0.35)
}
.frame(width: g.size.width * 0.80, height: g.size.height * 0.35)
.border(Color(.systemBlue), width: 4)
Related
I have an app that allows the user to assign expenses to selected categories. The Setup Categories view contains two boxes Available Categories and Current Categories. Tapping on a category in the Available Categories box moves it to the Current Categories box. The opposite is also true: tapping on a category in the Current Categories box moves it to the Available Categories box. A category in the Current Category box may not be moved to the Available box if the category has a nonzero balance.
The problem that I see is the ability to remove a category from the Current Categories box after a transaction has just been recorded. The transaction can be confirmed by looking at the history view, category totals view, and debug code in the Setup Categories view. But upon going to the Category Update view, I can now delete the category. If I don't move the category out of the Current Categories box and restart the app, then the category is prevented from being moved out.
The category data is filtered to prevent gaps from appearing in the category listings. I believe this data is not getting updated correctly. The method updateStatus is only called when the category structure is read. I would like to call this from Update Categories but it is not allowed because it is a view (Type '()' cannot conform to 'View'). Any ideas on how to fix this?
Note: I didn't include GetAvailableCats() since it is almost identical to GetCurrentCats() except that it uses filteredNotInUse.
// working categories
struct CatModel: Codable, Identifiable, Hashable {
var id = UUID()
var catName: String // category name
var catTotal: Double // category total
var catBudget: Double // category budget
var catPix: String // sf symbol
var catShow: Bool // true: in Use, false: available
}
class Categories: ObservableObject {
#Published var filteredInUse: [CatModel] = []
#Published var filteredNotInUse: [CatModel] = []
#Published var catItem: [CatModel] {
didSet {
if let encoded = try? JSONEncoder().encode(catItem) {
UserDefaults.standard.set(encoded, forKey: StorageKeys.workCat.rawValue)
}
}
}
init() {
if let catItem = UserDefaults.standard.data(forKey: StorageKeys.workCat.rawValue) {
if let decoded = try? JSONDecoder().decode([CatModel].self, from: catItem) {
self.catItem = decoded
self.updateStatus()
return
}
}
catItem = []
let item0 = CatModel(catName: "Category A", catTotal: 0.0, catBudget: 0, catPix: "scribble", catShow: false)
self.catItem.append(item0)
let item1 = CatModel(catName: "Category B", catTotal: 0.0, catBudget: 0, catPix: "scribble", catShow: false)
self.catItem.append(item1)
let item2 = CatModel(catName: "Category C", catTotal: 0.0, catBudget: 0, catPix: "scribble", catShow: false)
self.catItem.append(item2)
self.updateStatus()
}
func updateStatus() {
filteredInUse = catItem.filter({ (user) -> Bool in
return user.catShow == true
})
filteredNotInUse = catItem.filter({ (user) -> Bool in
return user.catShow == false
})
}
func addNewCatetory(catName: String) -> () {
let item = CatModel(catName: catName, catTotal: 0.0, catBudget: 0, catPix: "person.2")
catItem.append(item)
}
struct ShowCategories: View {
#State private var presentAlert = false
#State private var presentDetail: String = ""
var g: GeometryProxy
var body: some View {
VStack {
Text("Current Categories")
ScrollView {
GetCurrentCats(g: g, presentAlert: $presentAlert, presentDetail: $presentDetail)
} .frame(width: g.size.width * 0.80, height: g.size.height * 0.35)
.border(Color(.systemBlue), width: 4)
Text("Available Categories")
ScrollView {
GetAvailableCats(g: g)
} .frame(width: g.size.width * 0.80, height: g.size.height * 0.35).border(Color(.systemBlue), width: 4)
}.alert(
"Unable to remove category.", isPresented: $presentAlert,
presenting: presentDetail
) { detail in
Button("OK") {}
} message: { detail in
Text("\(presentDetail) category has a nonzero balance.")
}
.navigationBarTitle("Setup Categories", displayMode: .inline)
}
}
struct GetCurrentCats: View {
var g: GeometryProxy
#State private var showStatus: Bool = false
#Binding var presentAlert: Bool
#Binding var presentDetail: String
#EnvironmentObject var categories: Categories
var body: some View {
VStack {
ForEach(categories.filteredInUse, id: \.id) { item in
Button(action: {
if item.catTotal == 0.0 {
showStatus = false
} else {
presentAlert = true
presentDetail = item.catName
}
}){
HStack {
Image(systemName: item.catPix)
.resizable()
// .foregroundColor(Color(colors[index]))
.frame(width: 25, height: 25)
Text(item.catName)
.padding(.horizontal, 8)
.background(item.catTotal != 0.0 ? Color(.systemRed) : Color(.systemBlue))
.foregroundColor(Color.white)
.cornerRadius(8)
}
}
}
}
}
}
I have the following problem:
I built an app that has 6 buttons and each of these buttons is assigned a CL region.
I have a GPS track file that provides the app with locations and simulates a walk.
Now buttons should turn yellow when the region assigned to them is reached. It works so far, but the problem is that I don't know where to ask whether or not to reach the Region of Button.
I tried .onAppear () {if ...}, but that is only triggered once and I need something that monitors the If statement all the time.
Is there anything?
Thanks in advance
here my code:
import MapKit
import SwiftUI
import CoreLocation
var region1 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.505088, longitude: 13.333574), radius: 20, identifier: "region1")
var region2 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.504829, longitude: 13.336798), radius: 20, identifier: "region2")
var region3 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.504733, longitude: 13.341834), radius: 20, identifier: "region3")
var region4 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.503312, longitude: 13.347718), radius: 20, identifier: "region4")
var region5 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.505699, longitude: 13.352198), radius: 20, identifier: "region5")
var region6 = CLCircularRegion(center:CLLocationCoordinate2D(latitude: 52.513517, longitude: 13.350253), radius: 20, identifier: "region6")
class LocationEnviroment: NSObject, CLLocationManagerDelegate, ObservableObject{
var locationManager = CLLocationManager()
#Published var currentPosition = ""
var Waypoints = [region1, region2, region3, region4, region5, region6];
#Published var RegionIndex: Int = 6
func initPositionService(){
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
locationManager.startUpdatingLocation()
}
func isWayPoint(mylocation : CLLocation) -> Int{
for i in self.Waypoints{
if(i == mylocation){
if(i.identifier == "region1"){
return 0
}else if(i.identifier == "region2"){
return 1
}else if(i.identifier == "region3"){
return 2
}else if(i.identifier == "region4"){
return 3
}else if(i.identifier == "region5"){
return 4
}else if(i.identifier == "region6"){
return 5
}
}
}
return 6 // <- nothing found
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let myLocation = locations.last
let lat = myLocation?.coordinate.latitude
let lon = myLocation?.coordinate.longitude
self.currentPosition = "lat: \(lat ?? 0) lon: \(lon ?? 0)"
self.RegionIndex = isWayPoint(mylocation: myLocation!)
}
}
struct ContentView: View {
#StateObject var locationEnviroment = LocationEnviroment()
var Todo = ["Friseur / Haare schneiden", "Dönerbude / zu Mittag essen", "Bank / Geld abheben", "Supermarkt / einkaufen", "Blumeneladen /\n Blumen kaufen", " Lisa's Wohnung /\n Lisa Blumen überreichen" ]
#State private var disabled = Array(repeating: true, count: 6)
#State private var red = 255.0
#State private var blue = 0.0
#State private var green = 0.0
var body: some View {
VStack(alignment: .center, spacing: 10){
Text(locationEnviroment.currentPosition)
.padding()
ForEach(0..<6) { row in
Button(action : {
if(disabled[row] == false){
red = 0.0
green = 255.0
blue = 0.0
}
}) {
Text(Todo[row])
.frame(width : 200, height: 40, alignment: .center)
.clipShape(Rectangle())
.padding()
.border(Color.black, width: 1)
.font(.system(size: 15))
.foregroundColor(.black)
}
.background(Color(red: red/255, green: green/255, blue: blue/255))
.lineLimit(nil)
.cornerRadius(8.0)
.disabled(disabled[row])
.onAppear(){
if(locationEnviroment.RegionIndex == row){
disabled[row] = false
red = 255.0
green = 255.0
blue = 0.0
}
}
}
}
.onAppear(){
locationEnviroment.initPositionService()
}
}
}
You can use onChange to monitor the status of RegionIndex. It might look like this (replacing your onAppear):
.onChange(of: locationEnviroment.RegionIndex) { regionIndex in
if regionIndex == row {
disabled[row] = false
red = 255.0
green = 255.0
blue = 0.0
}
}
A couple of side notes:
Generally in Swift, variable and property names start with lowercase letters
You may want to investigate using computed properties to change/determine the colors. I started writing a solution with those but then realized I didn't totally understand what the full logic of the color system was, especially with the interaction between pressing and disabling the buttons. But, in general, your view will re-render any time a #Published property on your #StateObject changes, so you don't necessarily have to use something like onChange to trigger an update.
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)
}
}
Previously I did with Swift4 UIScrollView which scrolled with buttons and x offset.
In Swift4 I have:
Set Scrolling Enabled and Paging Enabled to false.
Created the margins, offsets for each frame in UIScrollView and changed the position with buttons Back and Next.
Here is the code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var buttonSound: UIButton!
#IBOutlet weak var buttonPrev: UIButton!
#IBOutlet weak var buttonNext: UIButton!
#IBOutlet weak var scrollView: UIScrollView!
var levels = ["level1", "level2", "level3", "level4"]
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
var currentLevel = 1
var previousLevel: Int? = nil
override func viewDidLoad() {
super.viewDidLoad()
//Defining the Various Swipe directions (left, right, up, down)
let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(self.handleGesture(gesture:)))
swipeLeft.direction = .left
self.view.addGestureRecognizer(swipeLeft)
let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(self.handleGesture(gesture:)))
swipeRight.direction = .right
self.view.addGestureRecognizer(swipeRight)
addHorizontalLevelsList()
customizeButtons()
resizeSelected()
}
func addHorizontalLevelsList() {
var frame : CGRect?
for i in 0..<levels.count {
let button = UIButton(type: .custom)
let buttonW = screenWidth/3
let buttonH = screenHeight/2
frame = CGRect(x: CGFloat(i+1) * (screenWidth/2) - (buttonW/2),
y: buttonH - 100,
width: buttonW,
height: buttonH)
button.frame = frame!
button.tag = i+1
button.backgroundColor = .lightGray
button.addTarget(self, action: #selector(selectTeam), for: .touchUpInside)
button.setTitle(levels[i], for: .normal)
scrollView.addSubview(button)
}
scrollView.contentSize = CGSize(width: (screenWidth/2 * CGFloat(levels.count)),
height: screenHeight)
scrollView.backgroundColor = .clear
self.view.addSubview(scrollView)
}
func customizeButtons(){
buttonPrev.frame = CGRect(x: 0,
y: (screenHeight/2) - 40,
width: 80, height: 80)
buttonNext.frame = CGRect(x: screenWidth - 80,
y: (screenHeight/2) - 40,
width: 80, height: 80)
buttonPrev.superview?.bringSubviewToFront(buttonPrev)
buttonNext.superview?.bringSubviewToFront(buttonNext)
}
#objc func selectTeam(button: UIButton) {
button.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
UIView.animate(withDuration: 1.0,
delay: 0,
usingSpringWithDamping: CGFloat(0.20),
initialSpringVelocity: CGFloat(6.0),
options: UIView.AnimationOptions.allowUserInteraction,
animations: {
button.transform = CGAffineTransform.identity
},
completion: { Void in() }
)
print(levels[button.tag])
let vc = PopTypeVC(nibName: "PopTypeVC", bundle: nil)
vc.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
self.present(vc, animated: true)
}
#IBAction func prevLevel(_ sender: Any) {
if currentLevel > 0 {
currentLevel -= 1
scroll()
}
}
#IBAction func nextLevel(_ sender: Any) {
if currentLevel < levels.count {
currentLevel += 1
scroll()
}
}
func scroll(){
print(currentLevel)
print(previousLevel as Any)
scrollView.setContentOffset(CGPoint(x: currentLevel * Int(screenWidth/2), y: 0), animated: true)
resizeSelected()
}
// The #objc before func is a must, since we are using #selector (above)
#objc func handleGesture(gesture: UISwipeGestureRecognizer) -> Void {
if gesture.direction == UISwipeGestureRecognizer.Direction.right {
prevLevel(self)
}
else if gesture.direction == UISwipeGestureRecognizer.Direction.left {
nextLevel(self)
}
}
func resizeSelected(){
if previousLevel != nil {
let previousFrame = CGRect(x:CGFloat(previousLevel!) * (screenWidth/2) - (screenWidth/3)/2,
y: (screenHeight/2) - 100,
width: screenWidth/3,
height: screenHeight/2)
scrollView.viewWithTag(previousLevel!)?.frame = previousFrame
}
let currentFrame = CGRect(x: CGFloat(currentLevel) * (screenWidth/2) - (screenWidth/3)/2 - 10,
y: (screenHeight/2) - 110,
width: screenWidth/3 + 20,
height: screenHeight/2 + 20)
scrollView.viewWithTag(currentLevel)?.frame = currentFrame
previousLevel = currentLevel
}
}
The problem is I can't do this with SwiftUI:
struct ContentView: View {
static var levels = ["level1",
"level2",
"level3",
"level4"]
var currentLevel = 1
var previousLevel: Int? = nil
let screenW = UIScreen.main.bounds.width
let screenH = UIScreen.main.bounds.height
let margin1 = 50
let margin2 = 100
let margin3 = 20
let sceneButtonW = 100
let buttonPadding = 40
var body: some View {
ZStack {
// Horizontal list
VStack {
Spacer()
.frame(height: margin2)
ScrollView(.horizontal, showsIndicators: false) {
HStack{
Spacer()
.frame(width: buttonPadding + sceneButtonW/2)
ForEach(0..<ContentView.levels.count) { i in
cardView(i: i).tag(i+1)
}
Spacer()
.frame(width: buttonPadding + sceneButtonW/2)
}
}
Spacer()
.frame(height: margin3)
}
}
.background(Image("bg")
.resizable()
.edgesIgnoringSafeArea(.all)
.aspectRatio(contentMode: .fill))
}
}
Question: Are there any methods to disable automatic scrolling at all and use offsets at ScrollView with SwiftUI?
This already built solution for SwiftUI
https://github.com/fermoya/SwiftUIPager
However, there is no real example.
struct Flashcard: View {
#State var tangoID = randomNum
#State var refreshToggle = false
#State var showingSheet = false
#State var bookmarked = tangoArray[randomNum].bookmark
#State public var showingNoMoreCardsSheet = false
#State private var showResults: Bool = false
#State private var fullRotation: Bool = false
var body: some View {
let zstack = ZStack {
Frontside(id: $tangoID, sheet: $showingSheet, rotate: $fullRotation)
.rotation3DEffect(.degrees(self.showResults ? 180.0 : 0.0), axis: (x: 0.0, y: 1.0, z: 0.0))
.rotation3DEffect(.degrees(self.fullRotation ? 360.0 : 0.0), axis: (x: 0.0, y: 1.0, z: 0.0))
.zIndex(self.showResults ? 0 : 1)
Backside(id: $tangoID, sheet: $showingSheet, bookmark: $bookmarked, results: $showResults, rotate: $fullRotation)
.rotation3DEffect(.degrees(self.showResults ? 0.0 : 180.0), axis: (x: 0.0, y: -1.0, z: 0.0))
.rotation3DEffect(.degrees(self.fullRotation ? 360.0 : 0.0), axis: (x: 0.0, y: 1.0, z: 0.0))
.zIndex(self.showResults ? 1 : 0)
}
.actionSheet(isPresented: $showingSheet) {
ActionSheet(title: Text("Finished 終わり"), message: Text("お疲れさま! But feel free to keep going."), buttons: [.default(Text("はい"))]);
}
.onTapGesture {
self.handleFlipViewTap()
}
.navigationBarTitle("Study")
.contextMenu(menuItems: {Button(action: {
tangoArray[randomNum].bookmark.toggle()
database.updateUserData(tango: tangoArray[randomNum])
self.fullRotation.toggle()
}, label: {
VStack{
Image(systemName: tangoArray[randomNum].bookmark ? "bookmark" : "bookmark.fill")
.font(.title)
Text(tangoArray[randomNum].bookmark ? "Remove bookmark" : "Bookmark")
}
})
})
return zstack
}
private func handleFlipViewTap() -> Void
{
withAnimation(.linear(duration: 0.25))
{
self.showResults.toggle()
}
}
}
public struct Frontside: View
{
#Binding public var id: Int
public var body: some View
{
ZStack{
RoundedRectangle(cornerRadius: 8, style: .continuous)
.frame(width: 140, height: 149)
.zIndex(0)
VStack {
Text(tangoArray[self.id].kanji)
.font(.system(size: 24))
.fontWeight(.regular)
.padding(25)
.lineLimit(3)
.zIndex(1)
Spacer()
}
VStack {
Spacer()
HStack {
Button(action: {
incorrect(i: self.id)
checkAttempts()
self.id = nextCard()
}) {
Image(systemName: "xmark")
.font(.headline)
.opacity(0.4)
}
Button(action: {
correct(i: self.id)
checkAttempts()
self.id = nextCard()
}) {
Image(systemName: "circle")
.font(.headline)
.opacity(0.4)
}
}
}
.zIndex(2)
}
}
}
I have a view which is a flashcard. When the user taps on the incorrect button I want the flash card to slide to the left of the screen, and when the user taps on the correct button I want the flash card to transition/slide to the right of the watch screen. How do I do that?
When the user taps on the incorrect button I want the flash card to
slide to the left of the screen, and when the user taps on the correct
button I want the flash card to transition/slide to the right of the
watch screen
I created a minimum viable example to show you a possible solution. Take a look and tell me if I can help you more.
struct ContentView: View {
private let objWidth = CGFloat(100)
private let objHeight = CGFloat(200)
private let screenWidth = UIScreen.main.bounds.size.width;
private let screenHeight = UIScreen.main.bounds.size.height;
#State private var objOffset = CGFloat(50)
var body: some View {
VStack {
Rectangle()
.frame(width: objWidth, height: objHeight)
.background(Color.black)
.position(x: objOffset, y: (screenHeight-objHeight)/2.0)
Button(action: {
withAnimation{
self.move()
}
}) {
Text("TAP")
}
}
}
private func move() {
if objOffset > screenWidth/2.0 {
objOffset = objWidth/2.0
} else {
objOffset = screenWidth-objWidth/2.0
}
}
}