How can I make a Form in which the elements are automatically divided into sections based on their first letter and add to the right the alphabet jumper to show the elements starting by the selected letter (just like the Contacts app)?
I also noted a strange thing that I have no idea how to recreate: not all letters are shown, some of them appear as "•". However, when you tap on them, they take you to the corresponding letter anyway. I tried using a ScrollView(.vertical) inside a ZStack and adding .scrollTo(selection) into the action of the Buttons, however 1) It didn't scroll to the selection I wanted 2) When I tapped on the "•", it was as if I was tapping on all of them because they all did the tapping animation 3) I wasn't able to divide the List as I wanted to.
I have this:
import SwiftUI
struct ContentView: View {
let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W", "X","Y", "Z"]
let values = ["Avalue", "Bvalue", "Cvalue", "Dvalue"]
var body: some View {
ScrollViewReader{ scrollviewr in
ZStack {
ScrollView(.vertical) {
VStack {
ForEach(alphabet, id: \.self) { letters in
Button(letters){
withAnimation {
scrollviewr.scrollTo(letters)
}
}
}
}
}.offset(x: 180, y: 120)
VStack {
ForEach(values, id: \.self){ vals in
Text(vals).id(vals)
}
}
}
}
}
}
But I'd want it like this:
import SwiftUI
struct AlphabetSort2: View {
let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W", "X","Y", "Z"]
let values = ["Avalue", "Bvalue", "Cvalue", "Dvalue", "Mvalue", "Zvalue"]
var body: some View {
ScrollView {
ScrollViewReader { value in
ZStack{
List{
ForEach(alphabet, id: \.self) { letter in
Section(header: Text(letter)) {
ForEach(values.filter { $0.hasPrefix(letter) }, id: \.self) { vals in
Text(vals).id(vals)
}
}.id(letter)
}
}
HStack{
Spacer()
VStack {
ForEach(0..<alphabet.count, id: \.self) { idx in
Button(action: {
withAnimation {
value.scrollTo(alphabet[idx])
}
}, label: {
Text(idx % 2 == 0 ? alphabet[idx] : "\u{2022}")
})
}
}
}
}
}
}
}
}
struct AlphabetSort2_Previews: PreviewProvider {
static var previews: some View {
AlphabetSort2()
}
}
Add an swipe-up-and-down action for the alphabet jumper, so we can get an UILocalizedIndexCollation -like swipe effect, it works for view and add mode, but delete, I guess it is due to SwiftUI's UI refresh mechanism.
extension String {
static var alphabeta: [String] {
var chars = [String]()
for char in "abcdefghijklmnopqrstuvwxyz#".uppercased() {
chars.append(String(char))
}
return chars
}
}
struct Document: View {
#State var items = ["Alpha", "Ash", "Aman", "Alisia", "Beta", "Baum", "Bob", "Bike", "Beeber", "Beff", "Calipha", "Cask", "Calf", "Deamon", "Deaf", "Dog", "Silk", "Seal", "Tiger", "Tom", "Tan", "Tint", "Urshinabi", "Verizon", "Viber", "Vein", "Wallet", "Warren", "Webber", "Waiter", "Xeon", "Young", "Yoda", "Yoga", "Yoger", "Yellow", "Zeta"]
var body: some View {
ScrollViewReader { scrollView in
HStack {
List {
ForEach(String.alphabeta, id: \.self){ alpha in
let subItems = items.filter({$0.starts(with: alpha)})
if !subItems.isEmpty {
Section(header: Text(alpha)) {
ForEach(subItems, id: \.self) { item in
Text(item)
}.onDelete(perform: { offsets in
items.remove(at: offsets.first!)
})
}.id(alpha)
}
}
}
VStack{
VStack {
SectionIndexTitles(proxy: scrollView, titles: retrieveSectionTitles()).font(.footnote)
}
.padding(.trailing, 10)
}
}
}
// .navigationBarTitleDisplayMode(.inline)
// .navigationBarHidden(true)
}
func retrieveSectionTitles() ->[String] {
var titles = [String]()
titles.append("#")
for item in self.items {
if !item.starts(with: titles.last!){
titles.append(String(item.first!))
}
}
titles.remove(at: 0)
if titles.count>1 && titles.first! == "#" {
titles.append("#")
titles.removeFirst(1)
}
return titles
}
}
struct Document_Previews: PreviewProvider {
static var previews: some View {
Document()
}
}
struct SectionIndexTitles: View {
class IndexTitleState: ObservableObject {
var currentTitleIndex = 0
var titleSize: CGSize = .zero
}
let proxy: ScrollViewProxy
let titles: [String]
#GestureState private var dragLocation: CGPoint = .zero
#StateObject var indexState = IndexTitleState()
var body: some View {
VStack {
ForEach(titles, id: \.self) { title in
Text(title)
.foregroundColor(.blue)
.modifier(SizeModifier())
.onPreferenceChange(SizePreferenceKey.self) {
self.indexState.titleSize = $0
}
.onTapGesture {
proxy.scrollTo(title, anchor: .top)
}
}
}
.gesture(
DragGesture(minimumDistance: indexState.titleSize.height, coordinateSpace: .named(titles.first))
.updating($dragLocation) { value, state, _ in
state = value.location
scrollTo(location: state)
}
)
}
private func scrollTo(location: CGPoint){
if self.indexState.titleSize.height > 0{
let index = Int(location.y / self.indexState.titleSize.height)
if index >= 0 && index < titles.count {
if indexState.currentTitleIndex != index {
indexState.currentTitleIndex = index
print(titles[index])
DispatchQueue.main.async {
let impactMed = UIImpactFeedbackGenerator(style: .medium)
impactMed.impactOccurred()
// withAnimation {
proxy.scrollTo(titles[indexState.currentTitleIndex], anchor: .top)
// }
}
}
}
}
}
}
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct SizeModifier: ViewModifier {
private var sizeView: some View {
GeometryReader { geometry in
Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size)
}
}
func body(content: Content) -> some View {
content.background(sizeView)
}
}
I have an issue where I am calling a function, it's appending an array, and then I get a random element from it, the problem is that is keeps getting wiped right after the function is called before I can use it, therefore, the random element request is causing a fatal error.
Utilities.getKnockKnockJokes {
self.previousKnockKnockJoke = self.currentKnockKnockJoke
print("currentKnockKnockJoke = \(self.currentKnockKnockJoke)")
self.currentKnockKnockJoke = KnockKnockJokes.knockKnockJokes.randomElement()!
print("newCurrentKnockKnockJoke = \(self.currentKnockKnockJoke)")
self.singleServeText.text = self.currentKnockKnockJoke
}
The function called is below:
static func getKnockKnockJokes(completion: #escaping () -> Void) {
let db = Firestore.firestore()
let group = DispatchGroup()
group.enter()
DispatchQueue.global(qos: .background).async {
print("countKnockKnockJokes = \(self.countKnockKnockJokes)")
if self.countKnockKnockJokes == 0 {
while self.countKnockKnockJokes == 0 {
if self.countKnockKnockJokes == 0 {
self.countKnockKnockJokes = 1
group.leave()
}else {
print("leaving")
group.leave()
}
}
}else {
print("skipped")
group.leave()
}
}
group.notify(queue: .main) {
db.collection("jokes").document("Knock Knock Jokes").addSnapshotListener { document, error in
//check for error
if error == nil {
//check if document exists
if document != nil && document!.exists {
if let JokeNum = document!.get("JokeNum") as? Int {
self.countKnockKnockJokes = JokeNum
UserDefaults.standard.setValue(JokeNum, forKey: "countKnockKnockJokes")
print("KnockKnockJokeNum = \(self.countKnockKnockJokes)")
}
var count = 1
print("count = \(count)/\(self.countKnockKnockJokes)")
print("countKnockKnockJoke = \(self.countKnockKnockJokes)")
//Utilities.knockKnockJokes.removeAll()
KnockKnockJokes.knockKnockJokes.removeAll()
for _ in 0...self.countKnockKnockJokes {
print("count = \(count)/\(self.countKnockKnockJokes)")
if let Joke = document!.get("\(count)") as? String {
print("KnockKnockJokeNum = \(self.countKnockKnockJokes)")
if Utilities.jokes.contains("\(Joke) - From Knock Knock Jokes") {}else {
print("Joke = \(Joke)")
Utilities.jokes.append("\(Joke) - From Knock Knock Jokes")
KnockKnockJokes.knockKnockJokes.append(Joke)
print("KnockKnockJokes = \(KnockKnockJokes.knockKnockJokes)")
UserDefaults.standard.set(KnockKnockJokes.knockKnockJokes, forKey: defaults.knockKnockJokes.rawValue)
Utilities.updateJokesDefaults()
}
print("countKnockKnockFinal = \(count)/\(self.countKnockKnockJokes)")
if count == self.countKnockKnockJokes {
completion()
}
count = count + 1
}
}
}
}
}
}
}
try this:
if count == self.countKnockKnockJokes {
completion()
return // <--- here
}
I fixed it, my remove all was running after all the appends.
I'm new in swiftUI developing, and I have a little problem. I made a function using Timer to create a count up timer. I want to display the time updating in a TextField, but it doesn't work (with a Text it works). Here is the code:
import SwiftUI
struct ContentView: View {
#State var min: Int = 0
#State var sec: Int = 0
#State var time = ""
#State var timer: Timer? = nil
var body: some View {
NavigationView{
VStack{
TextField("Tempo (min o mm:ss)", text: $time)
HStack{
Button(action: {
self.start()
self.time = String(self.min)+":"+String(self.sec)
}){
Text("Start")
}
Button(action: {
self.stop()
}){
Text("Stop")
}
}
}//VStack
}//NavigationView
}//body
func start(){
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){
temp in
self.sec = self.sec + 1
if(self.sec == 59){
self.sec = 0
self.min = self.min + 1
}
}
}
func stop(){
timer?.invalidate()
timer = nil
}
}//contetView
You don't see the change because you're not updating the time String in your timer loop.
Here's the fix:
func updateTimeString() {
self.time = String(format: "%02d:%02d", self.min, self.sec)
}
func start(){
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { temp in
self.sec = self.sec + 1
if self.sec == 59 {
self.sec = 0
self.min = self.min + 1
}
updateTimeString()
}
}
Notes:
I moved the code to set the time string to a separate function. You can then call this from your Button as well.
I used String(format:) to add formatting to your string to show 2 digits for the minutes and seconds.
I am using a timer in a SwiftUI view as in code below. It works as expected BUT under some conditions I want to cancel/stop that timer. There seems to be no ".cancel" property or method on the timer var. How do I cancel this timer? Any ideas/tips?
import SwiftUI
struct ContentView: View {
#State private var selection = 2
#State private var rotation: Double = GBstrtest
let timer = Timer.publish (every: 0.8, on: .current, in: .common).autoconnect()
var body: some View {
TabView(selection: $selection){
Text("Settings")
.font(.title)
.tabItem {
VStack {
Image(systemName: "gear")
.font(Font.system(.title ))
Text("Settings")
}
}
.tag(0)
VStack {
Divider().padding(2)
ZStack {
Image("image1")
.resizable()
.aspectRatio(contentMode: .fit)
Image("image2")
.resizable()
.aspectRatio(contentMode:.fit)
.rotationEffect(.degrees(rotation))
.animation(.easeInOut(duration: 0.3) )
.padding(EdgeInsets(top: 0, leading: 50, bottom: 0, trailing: 50))
}
Spacer()
}
.tabItem {
VStack {
Image(systemName: "speedometer")
.font(Font.system(.title ))
Text("Read Meter")
}
}
.tag(1)
}
.onReceive(timer) {
_ in self.rotation = Double.random(in: 0 ... 200)
// How do I cancel timer HERE!?
}
}
}
Inside your conditional statement, use the following code:
self.timer.upstream.connect().cancel()
Full cycle goes like this:
struct MySwiftUIView : View {
...
#State var timer = Timer.publish (every: 1, on: .current, in: .common).autoconnect()
#State var timeRemaining = -1
var body: some View {
ZStack {
// Underlying shapes etc as needed
Text("\(timeRemaining)").
.opacity(timeRemaining > 0 ? 1 : 0)
.onReceive(timer) { _ in
if self.timeRemaining < 0 {
// We don't need it when we start off
self.timer.upstream.connect().cancel()
return
}
if self.timeRemaining > 0 {
self.timeRemaining -= 1
} else {
self.timer.upstream.connect().cancel()
// Do the action on end of timer. Text would have been hidden by now
}
}
.onTapGesture { // Or any other event handler that should start countdown
self.timeRemaining = Int(delayValue)
self.timer = Timer.publish (every: 1, on: .current, in:
.common).autoconnect()
}
}
Voila! A reusable timer, use it as many times as you'd like!
Within my struct I have the following:
subscript(index: Int) -> FileSystemObject {
var i: Int = 0
for a in contents! {
if (i == index) {
return a
}
i++
}
}
Where,
var contents: FileSystemObject = [FileSystemObject]?
But when,
let it: FileSystemObject = FileSystemObject()
And I write:
return it.contents![index]
I receive the error
Cannot subscript a value of type [FileSystemObject]
What am I doing wrong here?
Additionally, note that:
Changing each of the objects with the value
FileSystemObject
To,
[FileSystemObject]
Does not help.
EDIT 1:
This is the entirety of the code:
MainWindowController.swift
class MainWindowController: NSWindowController, NSOutlineViewDataSource, NSOutlineViewDelegate {
#IBOutlet weak var sourceView: NSOutlineView!
static var fileManager: NSFileManager = NSFileManager.defaultManager()
static var fileSystem: FileSystemObject = FileSystemObject(path: "/", fs: fileManager)
var outlineSource: OutlinePrep = OutlinePrep(fs: fileSystem)
override func windowDidLoad() {
super.windowDidLoad()
sourceView.setDataSource(self)
sourceView.setDelegate(self)
}
func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
guard let it = item as? OutlinePrep else {
return outlineSource.basePath
}
return it.data[index]
}
func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {
// return (item == nil) ? YES : ([item numberOfChildren] != -1);
print(item)
guard let it = item as? OutlinePrep else {
return false
}
for (var i: Int = 0; i < it.data.count; i++) {
guard let _ = it.data[i].contents else {
return false
}
}
return true
}
func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {
guard let it = item as? OutlinePrep else {
return outlineSource.data.count
}
var i: Int = 0
for a in it.data {
guard let _ = a.contents else {
continue
}
i++
}
return i
}
}
FileSystem.swift
struct FileSystemObject {
let basePath: String
let name: String
var isDir = ObjCBool(false)
var contents: [FileSystemObject]?
init(path: String, fs: NSFileManager) {
basePath = path
let root: [String]
fs.fileExistsAtPath(path, isDirectory: &isDir)
if (isDir.boolValue) {
do {
root = try fs.contentsOfDirectoryAtPath(path)
}
catch {
root = ["Error"]
}
contents = []
for r in root {
contents!.append(FileSystemObject(path: (path + (r as String) + "/"), fs: fs))
}
}
name = path
}
subscript(index: Int) -> FileSystemObject {
get {
let error: FileSystemObject = FileSystemObject(path: "", fs: NSFileManager.defaultManager())
guard let _ = contents else {
return error
}
var i: Int = 0
for a in contents! {
if (i == index) {
return a
}
i++
}
return error
}
set {
}
}
}
Outline.swift
struct OutlinePrep {
var data: [FileSystemObject]
let basePath: String
private var cell: Int = -1
init (fs: FileSystemObject) {
data = fs.contents!
basePath = fs.basePath
}
mutating func outlineDelegate() -> String {
cell++
return data[cell].name
}
func testFunc(data: [FileSystemObject]) {
for (var i: Int = 0; i < data.count; i++) {
guard let d = data[i].contents else {
print(data[i].name)
continue
}
testFunc(d)
}
}
}
EDIT 2:
To clarify, I am inquiring as to how I might resolve the error, as all other provided code works as intended.
The error message is misleading. The problem becomes more apparent
if you split
return it.data[index]
into two separate statements:
func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
guard let it = item as? OutlinePrep else {
return outlineSource.basePath
}
let fso = it.data[index]
return fso // error: return expression of type 'FileSystemObject' does not conform to 'AnyObject'
}
The value of it.data[index] is a
FileSystemObject, which is a struct and therefore it
does not conform to AnyObject and cannot be the return value
of that method. If you want to return a FileSystemObject
then you have to define that as a class instead.
simplified ...
struct FileSystemObject {
var context: [FileSystemObject]?
init() {
context = []
// your init is called recursively, forever ...
let fso = FileSystemObject()
context?.append(fso)
}
}
let s = FileSystemObject()