Add countdown till when the next flashcard is going to show up
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
6CEF7F812BC4694900E205F6 /* WordAXCD.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 6CEF7F7F2BC4694900E205F6 /* WordAXCD.xcdatamodeld */; };
|
6CEF7F812BC4694900E205F6 /* WordAXCD.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 6CEF7F7F2BC4694900E205F6 /* WordAXCD.xcdatamodeld */; };
|
||||||
6CEF7F842BC46B5900E205F6 /* Flashcard+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEF7F822BC46B5900E205F6 /* Flashcard+CoreDataClass.swift */; };
|
6CEF7F842BC46B5900E205F6 /* Flashcard+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEF7F822BC46B5900E205F6 /* Flashcard+CoreDataClass.swift */; };
|
||||||
6CEF7FA32BC88F6000E205F6 /* Flashcard+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEF7FA12BC88F6000E205F6 /* Flashcard+CoreDataProperties.swift */; };
|
6CEF7FA32BC88F6000E205F6 /* Flashcard+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEF7FA12BC88F6000E205F6 /* Flashcard+CoreDataProperties.swift */; };
|
||||||
|
6CEF7FA62BC96F2B00E205F6 /* ButtonHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEF7FA52BC96F2B00E205F6 /* ButtonHStackView.swift */; };
|
||||||
6CF439522B83541D004C3543 /* WordAXApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF439512B83541D004C3543 /* WordAXApp.swift */; };
|
6CF439522B83541D004C3543 /* WordAXApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF439512B83541D004C3543 /* WordAXApp.swift */; };
|
||||||
6CF439542B83541D004C3543 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF439532B83541D004C3543 /* MainView.swift */; };
|
6CF439542B83541D004C3543 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF439532B83541D004C3543 /* MainView.swift */; };
|
||||||
6CF439562B83541E004C3543 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6CF439552B83541E004C3543 /* Assets.xcassets */; };
|
6CF439562B83541E004C3543 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6CF439552B83541E004C3543 /* Assets.xcassets */; };
|
||||||
@@ -42,6 +43,8 @@
|
|||||||
6CEF7F962BC6B45F00E205F6 /* WordAX0.0.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = WordAX0.0.1.xcdatamodel; sourceTree = "<group>"; };
|
6CEF7F962BC6B45F00E205F6 /* WordAX0.0.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = WordAX0.0.1.xcdatamodel; sourceTree = "<group>"; };
|
||||||
6CEF7F9F2BC88F3900E205F6 /* WordAX0.0.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = WordAX0.0.2.xcdatamodel; sourceTree = "<group>"; };
|
6CEF7F9F2BC88F3900E205F6 /* WordAX0.0.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = WordAX0.0.2.xcdatamodel; sourceTree = "<group>"; };
|
||||||
6CEF7FA12BC88F6000E205F6 /* Flashcard+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Flashcard+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
6CEF7FA12BC88F6000E205F6 /* Flashcard+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Flashcard+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||||
|
6CEF7FA42BC95F0300E205F6 /* WordAX0.0.3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = WordAX0.0.3.xcdatamodel; sourceTree = "<group>"; };
|
||||||
|
6CEF7FA52BC96F2B00E205F6 /* ButtonHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonHStackView.swift; sourceTree = "<group>"; };
|
||||||
6CF4394E2B83541D004C3543 /* WordAX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WordAX.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
6CF4394E2B83541D004C3543 /* WordAX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WordAX.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
6CF439512B83541D004C3543 /* WordAXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordAXApp.swift; sourceTree = "<group>"; };
|
6CF439512B83541D004C3543 /* WordAXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordAXApp.swift; sourceTree = "<group>"; };
|
||||||
6CF439532B83541D004C3543 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
|
6CF439532B83541D004C3543 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
|
||||||
@@ -70,6 +73,7 @@
|
|||||||
6C81850B2B8BA6BC0033CF46 /* FlashCardListRowView.swift */,
|
6C81850B2B8BA6BC0033CF46 /* FlashCardListRowView.swift */,
|
||||||
6C8185012B88C9FB0033CF46 /* SettingsView.swift */,
|
6C8185012B88C9FB0033CF46 /* SettingsView.swift */,
|
||||||
6C8185032B88CA210033CF46 /* AnkiView.swift */,
|
6C8185032B88CA210033CF46 /* AnkiView.swift */,
|
||||||
|
6CEF7FA52BC96F2B00E205F6 /* ButtonHStackView.swift */,
|
||||||
6C8185052B8A537F0033CF46 /* FlashCardView.swift */,
|
6C8185052B8A537F0033CF46 /* FlashCardView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
@@ -200,6 +204,7 @@
|
|||||||
6C8185062B8A537F0033CF46 /* FlashCardView.swift in Sources */,
|
6C8185062B8A537F0033CF46 /* FlashCardView.swift in Sources */,
|
||||||
6CEF7FA32BC88F6000E205F6 /* Flashcard+CoreDataProperties.swift in Sources */,
|
6CEF7FA32BC88F6000E205F6 /* Flashcard+CoreDataProperties.swift in Sources */,
|
||||||
6C81850A2B8BA5740033CF46 /* FlashCardListView.swift in Sources */,
|
6C81850A2B8BA5740033CF46 /* FlashCardListView.swift in Sources */,
|
||||||
|
6CEF7FA62BC96F2B00E205F6 /* ButtonHStackView.swift in Sources */,
|
||||||
6C8185002B88C9660033CF46 /* WordAXModelView.swift in Sources */,
|
6C8185002B88C9660033CF46 /* WordAXModelView.swift in Sources */,
|
||||||
6C8185042B88CA210033CF46 /* AnkiView.swift in Sources */,
|
6C8185042B88CA210033CF46 /* AnkiView.swift in Sources */,
|
||||||
6CEF7F7D2BC457E600E205F6 /* DataController.swift in Sources */,
|
6CEF7F7D2BC457E600E205F6 /* DataController.swift in Sources */,
|
||||||
@@ -419,6 +424,7 @@
|
|||||||
6CEF7F7F2BC4694900E205F6 /* WordAXCD.xcdatamodeld */ = {
|
6CEF7F7F2BC4694900E205F6 /* WordAXCD.xcdatamodeld */ = {
|
||||||
isa = XCVersionGroup;
|
isa = XCVersionGroup;
|
||||||
children = (
|
children = (
|
||||||
|
6CEF7FA42BC95F0300E205F6 /* WordAX0.0.3.xcdatamodel */,
|
||||||
6CEF7F9F2BC88F3900E205F6 /* WordAX0.0.2.xcdatamodel */,
|
6CEF7F9F2BC88F3900E205F6 /* WordAX0.0.2.xcdatamodel */,
|
||||||
6CEF7F962BC6B45F00E205F6 /* WordAX0.0.1.xcdatamodel */,
|
6CEF7F962BC6B45F00E205F6 /* WordAX0.0.1.xcdatamodel */,
|
||||||
6CEF7F802BC4694900E205F6 /* WordAX.xcdatamodel */,
|
6CEF7F802BC4694900E205F6 /* WordAX.xcdatamodel */,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class DataController: ObservableObject {
|
|||||||
flashcard.lastSeenOn = [Date().addingTimeInterval([86400, 24000, 100000].randomElement()!)].randomElement()!
|
flashcard.lastSeenOn = [Date().addingTimeInterval([86400, 24000, 100000].randomElement()!)].randomElement()!
|
||||||
flashcard.shownCount = [0, 1, 2, 3, 4, 5].randomElement()!
|
flashcard.shownCount = [0, 1, 2, 3, 4, 5].randomElement()!
|
||||||
flashcard.dateAdded = [Date(), Date().addingTimeInterval(-86400), Date().addingTimeInterval(-172800)].randomElement()!
|
flashcard.dateAdded = [Date(), Date().addingTimeInterval(-86400), Date().addingTimeInterval(-172800)].randomElement()!
|
||||||
|
// flashcard.calculatedNextRepetition = flashcard.lastSeenOn ?? Date() + TimeInterval(flashcard.nextSpacedRepetitionMilestone)
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
try viewContext.save()
|
try viewContext.save()
|
||||||
|
|||||||
@@ -11,6 +11,25 @@ import CoreData
|
|||||||
|
|
||||||
@objc(Flashcard)
|
@objc(Flashcard)
|
||||||
public class Flashcard: NSManagedObject {
|
public class Flashcard: NSManagedObject {
|
||||||
|
|
||||||
|
@objc dynamic var calculatedNextRepetition: Date {
|
||||||
|
if lastSeenOn != nil {
|
||||||
|
return lastSeenOn!.addSpacedRepetitionMilestone(milestone: self.getSpacedRepetitionMilestone())
|
||||||
|
} else {
|
||||||
|
return Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override public class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
|
||||||
|
// public override func value(forKey key: String) -> Any? {
|
||||||
|
let keyPaths = super.keyPathsForValuesAffectingValue(forKey: key)
|
||||||
|
if key == "calculatedNextRepetition" {
|
||||||
|
return keyPaths.union(Set(["lastSeenOn", "nextSpacedRepetitionMilestone"]))
|
||||||
|
} else {
|
||||||
|
return keyPaths
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
enum SpacedRepetitionMilestoneEnum: Int64, CaseIterable {
|
enum SpacedRepetitionMilestoneEnum: Int64, CaseIterable {
|
||||||
case Now = 0 // starting value
|
case Now = 0 // starting value
|
||||||
case OneMinute = 60 // 60 * 1
|
case OneMinute = 60 // 60 * 1
|
||||||
@@ -31,28 +50,28 @@ public class Flashcard: NSManagedObject {
|
|||||||
static func getNext(milestone: SpacedRepetitionMilestoneEnum?) -> SpacedRepetitionMilestoneEnum {
|
static func getNext(milestone: SpacedRepetitionMilestoneEnum?) -> SpacedRepetitionMilestoneEnum {
|
||||||
let sorted = SpacedRepetitionMilestoneEnum.allCasesSorted
|
let sorted = SpacedRepetitionMilestoneEnum.allCasesSorted
|
||||||
if milestone == nil {
|
if milestone == nil {
|
||||||
return SpacedRepetitionMilestoneEnum.TenMinutes
|
return .TenMinutes
|
||||||
}
|
}
|
||||||
let milestoneIndex = sorted.firstIndex(where: {$0.rawValue == milestone!.rawValue})!
|
let milestoneIndex = sorted.firstIndex(where: {$0.rawValue == milestone!.rawValue})!
|
||||||
if milestoneIndex < SpacedRepetitionMilestoneEnum.allCasesSorted.count {
|
if milestoneIndex < SpacedRepetitionMilestoneEnum.allCasesSorted.count {
|
||||||
return sorted[milestoneIndex + 1]
|
return sorted[milestoneIndex + 1]
|
||||||
}
|
}
|
||||||
return SpacedRepetitionMilestoneEnum.OneYear
|
return .OneYear
|
||||||
}
|
}
|
||||||
|
|
||||||
static func getMilestoneFromInt(value: Int64) -> SpacedRepetitionMilestoneEnum {
|
static func getMilestoneFromInt(value: Int64) -> SpacedRepetitionMilestoneEnum {
|
||||||
return SpacedRepetitionMilestoneEnum.allCasesSorted.first(where: {$0.rawValue == value}) ?? SpacedRepetitionMilestoneEnum.Now
|
return .allCasesSorted.first(where: {$0.rawValue == value}) ?? .Now
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func didChangeValue(forKey key: String) {
|
// public override func didChangeValue(forKey key: String) {
|
||||||
super.didChangeValue(forKey: key)
|
// super.didChangeValue(forKey: key)
|
||||||
if key == "lastSeenOn" || key == "nextSpacedRepetitionMilestone" {
|
// if key == "lastSeenOn" || key == "nextSpacedRepetitionMilestone" {
|
||||||
// updateCalculatedNextRepetition()
|
// // updateCalculatedNextRepetition()
|
||||||
calculatedNextRepetition = lastSeenOn ?? Date() + TimeInterval(nextSpacedRepetitionMilestone)
|
// calculatedNextRepetition = lastSeenOn ?? Date() + TimeInterval(nextSpacedRepetitionMilestone)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// func updateCalculatedNextRepetition() {
|
// func updateCalculatedNextRepetition() {
|
||||||
// if let lastSeen = lastSeenOn {
|
// if let lastSeen = lastSeenOn {
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ extension Flashcard {
|
|||||||
@NSManaged public var nextSpacedRepetitionMilestone: Int64
|
@NSManaged public var nextSpacedRepetitionMilestone: Int64
|
||||||
@NSManaged public var shown: Bool
|
@NSManaged public var shown: Bool
|
||||||
@NSManaged public var shownCount: Int64
|
@NSManaged public var shownCount: Int64
|
||||||
@NSManaged public var calculatedNextRepetition: Date?
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Flashcard : Identifiable {
|
extension Flashcard : Identifiable {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23D60" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
|
||||||
|
<entity name="Flashcard" representedClassName="Flashcard" syncable="YES">
|
||||||
|
<attribute name="dateAdded" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="desc" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="lastSeenOn" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="name" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="nextSpacedRepetitionMilestone" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="shown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="shownCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
</entity>
|
||||||
|
</model>
|
||||||
@@ -21,19 +21,18 @@ struct AnkiView: View {
|
|||||||
]),
|
]),
|
||||||
NSPredicate(format: "lastSeenOn == nil")
|
NSPredicate(format: "lastSeenOn == nil")
|
||||||
])) var flashcards: FetchedResults<Flashcard>
|
])) var flashcards: FetchedResults<Flashcard>
|
||||||
@FetchRequest(
|
|
||||||
sortDescriptors: [
|
@State private var timeRemaining = 10
|
||||||
NSSortDescriptor(key: "calculatedNextRepetition", ascending: false)
|
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||||
],
|
@State var sortedFlashcards: [Flashcard] = []
|
||||||
predicate:
|
|
||||||
NSCompoundPredicate(type: .and, subpredicates: [
|
@Environment(\.colorScheme) var colorScheme
|
||||||
NSPredicate(format: "%K != nil", "lastSeenOn"),
|
|
||||||
NSPredicate(
|
|
||||||
format: "lastSeenOn + nextSpacedRepetitionMilestone > %@", Date() as CVarArg)
|
|
||||||
]
|
|
||||||
)) var soonestFlashcard: FetchedResults<Flashcard>
|
|
||||||
|
|
||||||
// get the most recent flashcard
|
// get the most recent flashcard
|
||||||
|
@FetchRequest(sortDescriptors: [],
|
||||||
|
predicate: NSPredicate(format: "%K != nil", "lastSeenOn")) var soonestFlashcard: FetchedResults<Flashcard>
|
||||||
|
|
||||||
|
|
||||||
@State var showDescription = false
|
@State var showDescription = false
|
||||||
|
|
||||||
@@ -86,62 +85,32 @@ struct AnkiView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: Add countdown to the next available word
|
if !soonestFlashcard.isEmpty {
|
||||||
VStack{
|
Text("Next flashcard in: \(timeRemaining.convertDurationSecondsToCountdown())")
|
||||||
if !soonestFlashcard.isEmpty {
|
.foregroundStyle(.black)
|
||||||
Text("Time: \(timeRemaining.convertDurationSecondsToCountdown())")
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.vertical, 5)
|
|
||||||
.background(.black.opacity(0.75))
|
|
||||||
.clipShape(.capsule)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if !soonestFlashcard.isEmpty {
|
|
||||||
// Text("HERE")
|
|
||||||
// Text("\(soonestFlashcard.first!.lastSeenOn!.addingTimeInterval(TimeInterval(soonestFlashcard.first!.nextSpacedRepetitionMilestone)).timeIntervalSince(Date()))")
|
|
||||||
// Text("\(Int64(timeRemaining))")
|
|
||||||
// Text("\(soonestFlashcard.first!.lastSeenOn!)")
|
|
||||||
// Text("\(soonestFlashcard.first!.getSpacedRepetitionMilestone().rawValue)")
|
|
||||||
// Text("\(soonestFlashcard.first!.lastSeenOn!.addSpacedRepetitionMilestone(milestone: soonestFlashcard.first!.getSpacedRepetitionMilestone()))")
|
|
||||||
// }
|
|
||||||
// if soonestFlashcard.first! {
|
|
||||||
// Text("HERE")
|
|
||||||
// }
|
|
||||||
|
|
||||||
Text("There are currently no words to display")
|
|
||||||
.padding()
|
.padding()
|
||||||
.background(.yellow)
|
.background(.yellow)
|
||||||
.clipShape(.buttonBorder)
|
.clipShape(.buttonBorder)
|
||||||
}
|
.padding(.vertical, 50)
|
||||||
.onAppear {
|
.padding(.horizontal)
|
||||||
if !soonestFlashcard.isEmpty {
|
.background(.gray.opacity(0.3))
|
||||||
var lastSeen = soonestFlashcard.first!.lastSeenOn!
|
.clipShape(.buttonBorder)
|
||||||
var showIn = soonestFlashcard.first!.getSpacedRepetitionMilestone()
|
.onAppear {
|
||||||
var show = lastSeen.addSpacedRepetitionMilestone(milestone: showIn)
|
if !soonestFlashcard.isEmpty {
|
||||||
timeRemaining = Int(show.timeIntervalSinceNow)
|
sortedFlashcards = soonestFlashcard.sorted(by: {
|
||||||
}
|
($0.lastSeenOn!.addSpacedRepetitionMilestone(milestone:$0.getSpacedRepetitionMilestone()).timeIntervalSinceNow) < ($1.lastSeenOn!.addSpacedRepetitionMilestone(milestone: $1.getSpacedRepetitionMilestone()).timeIntervalSinceNow)
|
||||||
}
|
})
|
||||||
.onReceive(timer) { time in
|
timeRemaining = Int(sortedFlashcards.first!.lastSeenOn!.addSpacedRepetitionMilestone(milestone:sortedFlashcards.first!.getSpacedRepetitionMilestone()).timeIntervalSinceNow)
|
||||||
if !soonestFlashcard.isEmpty {
|
}
|
||||||
var lastSeen = soonestFlashcard.first!.lastSeenOn!
|
}
|
||||||
var showIn = soonestFlashcard.first!.getSpacedRepetitionMilestone()
|
.onReceive(timer) { time in
|
||||||
var show = lastSeen.addSpacedRepetitionMilestone(milestone: showIn)
|
if timeRemaining > 0 {
|
||||||
timeRemaining = Int(show.timeIntervalSinceNow)
|
timeRemaining -= 1
|
||||||
}
|
}
|
||||||
// timeRemaining = Int(soonestFlashcard.first!.lastSeenOn!.addSpacedRepetitionMilestone(milestone: soonestFlashcard.first!.getSpacedRepetitionMilestone()).timeIntervalSince(Date()))
|
}
|
||||||
// timeRemaining = Int(soonestFlashcard.first!.lastSeenOn!.addingTimeInterval(TimeInterval(soonestFlashcard.first!.nextSpacedRepetitionMilestone)).timeIntervalSince(Date()))
|
|
||||||
// if timeRemaining > 0 {
|
|
||||||
// timeRemaining -= 1
|
|
||||||
// }
|
|
||||||
// if !soonestFlashcard.isEmpty {
|
|
||||||
// timeRemaining = Int(Date().timeIntervalSince(soonestFlashcard.first!.lastSeenOn!))
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@State private var timeRemaining = 10
|
|
||||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
Reference in New Issue
Block a user