SwiftUI update progressBar in Thread - ios

I'm trying to manage a chat with SwiftUI.
I found a CircularProgressBar to work with SwiftUI, in the example, it's working with Timer.
If I change the timer with an Zip extraction, the UI doesn't update.
struct DetailView: View {
var selectedChat: Chat?
#State var progressBarValue:CGFloat = 0
var body: some View {
Group {
if selectedChat != nil {
VStack {
if progressBarValue < 1 {
CircularProgressBar(value: $progressBarValue)
}
else {
//Text("WELL DONE")
Text("\(UserData().getChat(withID: selectedChat!.id)!.allText.first!.text)")
}
}
} else {
VStack {
CircularProgressBar(value: $progressBarValue)
Text("Detail view content goes here")
}
}
}.navigationBarTitle(Text("\(selectedChat?.name ?? "")"))
.onAppear {
if let chat = self.selectedChat {
if chat.allText.count == 0 {
let exData = ExtractData()
if let path = chat.getUnzipPath()?.relativePath {
DispatchQueue.main.async {//with or without the behavior is the same
exData.manageExtractedZip(unzipPath: path) { progress in
if progress >= 1 {
var newChat = chat
newChat.allText = exData.allTexts
let userD = UserData()
userD.overrideChat(with: newChat)
print(exData.allTexts)
}
self.progressBarValue = CGFloat(progress)
print("progressBarValue: \(self.progressBarValue)") //This is printing well
}
}
}
}
else {
self.progressBarValue = 1
}
}
/* This is working
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
self.progressBarValue += 0.1
print(self.progressBarValue)
if (self.progressBarValue >= 1) {
timer.invalidate()
}
}*/
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(selectedChat: UserData().chatData.first!)
}
}
How to make it work?

Related

How do I switch views when the timer ends SwiftUI

I have two views. The Main View and a Break View. I have a timer running in the Main View which counts down to zero. When the timer reaches zero, I want to be able to switch the screen to Break View. I am using MVVM to keep track of the timers. Using .onReceive to make it look like the timer is running in the background.
I tried using a boolean to check if the timer has reached zero and based on that changed the view, but it's not working and is giving an error saying the result of the view is not used anywhere. I have a navigation view in the Content View if that's of any help.
Thanks in advance.
A snippet of the code :
Main View :
struct MainView: View {
var body: some View {
VStack(alignment: .center, spacing: 50, content: {
Button(action: {
if !fbManager.isTimerStarted {
fbManager.start()
fbManager.isTimerStarted = true
}
else {
fbManager.pause()
fbManager.isTimerStarted = false
}
}, label: {
Image(systemName: fbManager.isTimerStarted == true ? "pause.fill" : "play.fill")
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
.foregroundColor(Color(red: 1.00, green: 1.00, blue: 1.00))
})
.onReceive(NotificationCenter.default.publisher(
for: UIScene.didEnterBackgroundNotification)) { _ in
if fbManager.isTimerStarted {
movingToBackground()
}
}
.onReceive(NotificationCenter.default.publisher(
for: UIScene.willEnterForegroundNotification)) { _ in
if fbManager.isTimerStarted {
movingToForeground()
}
}
})
}
}
func movingToBackground() {
print("Moving to the background")
notificationDate = Date()
fbManager.pause()
}
func movingToForeground() {
print("Moving to the foreground")
let deltaTime: Int = Int(Date().timeIntervalSince(notificationDate))
fbManager.secondsElapsed -= deltaTime
fbManager.start()
}
}
View Model :
class FocusBreakManager: ObservableObject {
var timer: Timer = Timer()
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [self] _ in
self.secondsElapsed -= 1
self.focusfill += 0.01667
focusTime = String(secondsElapsed)
focusTime = formatCounter()
if secondsElapsed <= 0 {
stop()
}
}
}
func formatCounter() -> String {
let minutes = Int(secondsElapsed) / 60 % 60
let seconds = Int(secondsElapsed) % 60
return String(format : "%02i : %02i", minutes, seconds)
}
}
Hey to keep up with your solution here is an example of how that could work you would need to use #ObservedObject property wrapper in order to monitor updates from your view.
struct ContentView: View {
#ObservedObject private var focusBreakManager = FocusBreakManager()
var body: some View {
VStack {
Text("\(focusBreakManager.elapsedSeconds)")
Text(focusBreakManager.timerRunningMessage)
Button("Start timer", action: focusBreakManager.start)
}
.padding()
}
}
class FocusBreakManager: ObservableObject {
var timer: Timer = Timer()
#Published var elapsedSeconds = 0
var timerRunningMessage: String {
timerRunning
? "Timer is running"
: "Timer paused"
}
private var timerRunning: Bool {
timer.isValid
}
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
guard let self else { return }
self.elapsedSeconds += 1
if self.elapsedSeconds > 5 {
self.timer.invalidate()
}
}
}
}
You can also take a look at the autoconnect api here's a great tutorial:
https://www.hackingwithswift.com/books/ios-swiftui/triggering-events-repeatedly-using-a-timer

SwiftUI searchable modifier cancel button dismisses parent view's sheet presentation

I have a swiftui list with a .searchable modifier, contained within a navigation view child view, within a sheet presentation. When the Cancel button is used, not only is the search field dismissed (unfocused, keyboard dismissed etc), but the navigation view dismisses (ie moves back a page) and then the containing sheet view also dismisses.
This list acts as a custom picker view that allows the selection of multiple HKWorkoutActivityTypes. I use it in multiple locations and the bug only sometimes presents itself, despite being implemented in almost identical views. If I swap the .sheet for a .fullScreenCover, then the presentation of the bug swaps between those previously unaffected and those that were.
The parent view (implemented as a form item, in .sheet):
struct MultipleWorkoutActivityTypePickerFormItem: View, Equatable {
static func == (lhs: MultipleWorkoutActivityTypePickerFormItem, rhs: MultipleWorkoutActivityTypePickerFormItem) -> Bool {
return lhs.workoutActivityTypes == rhs.workoutActivityTypes
}
#Binding var workoutActivityTypes: [HKWorkoutActivityType]
var workoutActivityTypeSelectionDescription: String {
var string = ""
if workoutActivityTypes.count == 1 {
string = workoutActivityTypes.first?.commonName ?? ""
} else if workoutActivityTypes.count == 2 {
string = "\(workoutActivityTypes[0].commonName) & \(workoutActivityTypes[1].commonName)"
} else if workoutActivityTypes.count > 2 {
string = "\(workoutActivityTypes.first?.commonName ?? "") & \(workoutActivityTypes.count - 1) others"
} else {
string = "Any Workout"
}
return string
}
var body: some View {
NavigationLink {
MultipleWorkoutActivityTypePickerView(selectedWorkoutActivityTypes: $workoutActivityTypes)
.equatable()
} label: {
HStack {
Text("Workout Types:")
Spacer()
Text(workoutActivityTypeSelectionDescription)
.foregroundColor(.secondary)
}
}
}
}
And the child view:
struct MultipleWorkoutActivityTypePickerView: View, Equatable {
static func == (lhs: MultipleWorkoutActivityTypePickerView, rhs: MultipleWorkoutActivityTypePickerView) -> Bool {
return (lhs.favouriteWorkoutActivityTypes == rhs.favouriteWorkoutActivityTypes && lhs.selectedWorkoutActivityTypes == rhs.selectedWorkoutActivityTypes)
}
#State var searchString: String = ""
#Environment(\.dismissSearch) var dismissSearch
#Environment(\.isSearching) var isSearching
//MARK: - FUNCTIONS
#Binding var selectedWorkoutActivityTypes: [HKWorkoutActivityType]
#State var favouriteWorkoutActivityTypes: [FavouriteWorkoutActivityType] = []
#State var searchResults: [HKWorkoutActivityType] = HKWorkoutActivityType.allCases
let viewContext = PersistenceController.shared.container.viewContext
func loadFavouriteWorkoutActivityTypes() {
let request: NSFetchRequest<FavouriteWorkoutActivityType> = FavouriteWorkoutActivityType.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \FavouriteWorkoutActivityType.index, ascending: true)]
do {
try favouriteWorkoutActivityTypes = viewContext.fetch(request)
} catch {
print(error.localizedDescription)
}
print("FavouriteWorkoutActivityType - Load")
}
func updateSearchResults() {
if searchString.isEmpty {
searchResults = HKWorkoutActivityType.allCases
} else {
searchResults = HKWorkoutActivityType.allCases.filter { $0.commonName.contains(searchString) }
}
}
func createFavouriteWorkoutActivityType(workoutActivityType: HKWorkoutActivityType) {
let newFavouriteWorkoutActivityType = FavouriteWorkoutActivityType(context: viewContext)
newFavouriteWorkoutActivityType.workoutActivityTypeRawValue = Int16(workoutActivityType.rawValue)
newFavouriteWorkoutActivityType.index = Int16(favouriteWorkoutActivityTypes.count)
print(newFavouriteWorkoutActivityType)
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
loadFavouriteWorkoutActivityTypes()
}
func updateFavouriteWorkoutActivityTypeIndex(favouriteWorkoutActivityType: FavouriteWorkoutActivityType) {
//reoder
}
func deleteFavouriteWorkoutActivityType(favouriteWorkoutActivityType: FavouriteWorkoutActivityType) {
viewContext.delete(favouriteWorkoutActivityType)
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
loadFavouriteWorkoutActivityTypes()
}
func deleteFavouriteWorkoutActivityTypeFor(workoutActivityType: HKWorkoutActivityType) {
for favouriteWorkoutActivityType in favouriteWorkoutActivityTypes {
if favouriteWorkoutActivityType.workoutActivityTypeRawValue == workoutActivityType.rawValue {
viewContext.delete(favouriteWorkoutActivityType)
}
}
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
loadFavouriteWorkoutActivityTypes()
}
func deleteItems(offsets: IndexSet) {
offsets.map { favouriteWorkoutActivityTypes[$0] }.forEach(viewContext.delete)
renumberItems()
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
loadFavouriteWorkoutActivityTypes()
}
func renumberItems() {
if favouriteWorkoutActivityTypes.count > 1 {
for item in 1...favouriteWorkoutActivityTypes.count {
favouriteWorkoutActivityTypes[item - 1].index = Int16(item)
}
} else if favouriteWorkoutActivityTypes.count == 1 {
favouriteWorkoutActivityTypes[0].index = Int16(1)
}
// print("ChartsViewModel - Renumber")
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
loadFavouriteWorkoutActivityTypes()
}
func moveItems(from source: IndexSet, to destination: Int) {
// Make an array of items from fetched results
var revisedItems: [FavouriteWorkoutActivityType] = favouriteWorkoutActivityTypes.map{ $0 }
// change the order of the items in the array
revisedItems.move(fromOffsets: source, toOffset: destination)
// update the userOrder attribute in revisedItems to
// persist the new order. This is done in reverse order
// to minimize changes to the indices.
for reverseIndex in stride( from: revisedItems.count - 1,
through: 0,
by: -1 )
{
revisedItems[reverseIndex].index =
Int16(reverseIndex)
}
// print("ChartsViewModel - Move")
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
loadFavouriteWorkoutActivityTypes()
}
var body: some View {
List {
if searchString == "" {
Section {
ForEach(favouriteWorkoutActivityTypes) { favouriteWorkoutActivityType in
if let workoutActivityType = HKWorkoutActivityType(rawValue: UInt(favouriteWorkoutActivityType.workoutActivityTypeRawValue)) {
HStack {
HStack {
Label {
Text(workoutActivityType.commonName)
} icon: {
Image(systemName: (selectedWorkoutActivityTypes.contains(workoutActivityType) ? "checkmark.circle" : "circle"))
}
Spacer()
}
.contentShape(Rectangle())
.onTapGesture {
if !selectedWorkoutActivityTypes.contains(where: {$0 == workoutActivityType}) {
selectedWorkoutActivityTypes.append(workoutActivityType)
print("does not contain, adding:", workoutActivityType.commonName)
} else {
selectedWorkoutActivityTypes = selectedWorkoutActivityTypes.filter({$0 != workoutActivityType})
print("does contain, removing:", workoutActivityType.commonName)
}
}
Button {
withAnimation {
deleteFavouriteWorkoutActivityType(favouriteWorkoutActivityType: favouriteWorkoutActivityType)
}
} label: {
Image(systemName: "heart.fill")
}
.buttonStyle(BorderlessButtonStyle())
}
}
}
.onDelete(perform: deleteItems(offsets:))
.onMove(perform: moveItems(from:to:))
if favouriteWorkoutActivityTypes.count < 1 {
Text("Save your favourite workout types by hitting the hearts below.")
.foregroundColor(.secondary)
}
} header: {
Text("Favourites:")
}
}
Section {
ForEach(searchResults, id: \.self) { workoutActivityType in
HStack {
HStack {
Label {
Text(workoutActivityType.commonName)
} icon: {
Image(systemName: (selectedWorkoutActivityTypes.contains(workoutActivityType) ? "checkmark.circle" : "circle"))
}
Spacer()
}
.contentShape(Rectangle())
.onTapGesture {
if !selectedWorkoutActivityTypes.contains(where: {$0 == workoutActivityType}) {
selectedWorkoutActivityTypes.append(workoutActivityType)
print("does not contain, adding:", workoutActivityType.commonName)
} else {
selectedWorkoutActivityTypes = selectedWorkoutActivityTypes.filter({$0 != workoutActivityType})
print("does contain, removing:", workoutActivityType.commonName)
}
}
if favouriteWorkoutActivityTypes.contains(where: { FavouriteWorkoutActivityType in
workoutActivityType.rawValue == UInt(FavouriteWorkoutActivityType.workoutActivityTypeRawValue)
}) {
Button {
withAnimation {
deleteFavouriteWorkoutActivityTypeFor(workoutActivityType: workoutActivityType)
}
} label: {
Image(systemName: "heart.fill")
}
.buttonStyle(BorderlessButtonStyle())
} else {
Button {
withAnimation {
createFavouriteWorkoutActivityType(workoutActivityType: workoutActivityType)
}
} label: {
Image(systemName: "heart")
}
.buttonStyle(BorderlessButtonStyle())
}
}
}
} header: {
if searchString == "" {
Text("All:")
} else {
Text("Results:")
}
}
}
.searchable(text: $searchString.animation(), prompt: "Search")
.onChange(of: searchString, perform: { _ in
withAnimation {
updateSearchResults()
}
})
.onAppear {
loadFavouriteWorkoutActivityTypes()
}
.navigationBarTitle(Text("Type"), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
}
}

How to pass method handler back SwiftUI

I'm new at Swift and currently, I'm implementing UI for verification code but I have no idea to do I have found something that similar to my requirement on StackOverflow I just copy and pass into my project, and then as you can see in VerificationView_Previews we need to pass the method handler back i don't know how to pass it help, please
//
// VerificationView.swift
// UpdateHistory
//
// Created by Admin on 4/21/21.
//
import SwiftUI
public struct VerificationView: View {
var maxDigits: Int = 6
var label = "Enter One Time Password"
#State var pin: String = ""
#State var showPin = true
var handler: (String, (Bool) -> Void) -> Void
public var body: some View {
VStack {
Text(label).font(.title)
ZStack {
pinDots
backgroundField
}
}
}
private var pinDots: some View {
HStack {
Spacer()
ForEach(0..<maxDigits) { index in
Image(systemName: self.getImageName(at: index))
.font(.system(size: 60, weight: .thin, design: .default))
Spacer()
}
}
}
private func getImageName(at index: Int) -> String {
if index >= self.pin.count {
return "square"
}
if self.showPin {
return self.pin.digits[index].numberString + ".square"
}
return "square"
}
private var backgroundField: some View {
let boundPin = Binding<String>(get: { self.pin }, set: { newValue in
self.pin = newValue
self.submitPin()
})
return TextField("", text: boundPin, onCommit: submitPin)
.accentColor(.clear)
.foregroundColor(.clear)
.keyboardType(.numberPad)
}
private var showPinButton: some View {
Button(action: {
self.showPin.toggle()
}, label: {
self.showPin ?
Image(systemName: "eye.slash.fill").foregroundColor(.primary) :
Image(systemName: "eye.fill").foregroundColor(.primary)
})
}
private func submitPin() {
if pin.count == maxDigits {
handler(pin) { isSuccess in
if isSuccess {
print("pin matched, go to next page, no action to perfrom here")
} else {
pin = ""
print("this has to called after showing toast why is the failure")
}
}
}
}
}
extension String {
var digits: [Int] {
var result = [Int]()
for char in self {
if let number = Int(String(char)) {
result.append(number)
}
}
return result
}
}
extension Int {
var numberString: String {
guard self < 10 else { return "0" }
return String(self)
}
}
struct VerificationView_Previews: PreviewProvider {
static var previews: some View {
VerificationView() // need to pass method handler
}
}
For viewing purpose you can just simply use like this. No need second param for preview
struct VerificationView_Previews: PreviewProvider {
static var previews: some View {
VerificationView { (pin, _) in
print(pin)
}
}
}
You can also use like this
struct VerificationView_Previews: PreviewProvider {
var successClosure: (Bool) -> Void
static var previews: some View {
VerificationView { (pin, successClosure) in
}
}
}

SwiftUI: stuck on infinite page view implementation

I'm trying to create a PageView in pure SwiftUI. There's my test code below. And everything works as expected but the DragGesture. It just doesn't call 'onEnded' function. Never. How can I fix it?
struct PageView<V: View>: Identifiable {
let id = UUID()
var content: V
}
struct InfinitePageView: View {
#State private var pages: [PageView] = [
PageView(content: Text("Page")),
PageView(content: Text("Page")),
PageView(content: Text("Page"))
]
#State private var selectedIndex: Int = 1
#State private var isDragging: Bool = false
private var drag: some Gesture {
DragGesture()
.onChanged { _ in
self.isDragging = true
}
.onEnded { _ in
self.isDragging = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
resolvePages()
}
}
}
var body: some View {
NavigationView {
TabView(selection: $selectedIndex) {
ForEach(pages) { page in
page.content
.tag(pages.firstIndex(where: { $0.id == page.id })!)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.gesture(drag)
.onChange(of: selectedIndex, perform: { value in
guard !isDragging else { return }
DispatchQueue.main.async {
resolvePages()
}
})
}
}
private func resolvePages() {
if selectedIndex > 1 {
addNextPage()
}
if selectedIndex < 1 {
addPreviousPage()
}
}
private func addNextPage() {
pages.append(PageView(content: Text("Page")))
pages.removeFirst()
selectedIndex = 1
}
private func addPreviousPage() {
pages.insert(PageView(content: Text("Page")), at: 0)
pages.removeLast()
selectedIndex = 1
}
}
This is a known issue with SwiftUI
the DragGesture that you setup likely gets overridden by the DragGesture within the TabView.
Can detect onEnded with the setup in this post Detect DragGesture cancelation in SwiftUI but that deactivates the TabView/Page gestures.
Your onChange code for selectedIndex runs once the new page is selected and would act should act the same as onEnded.

SwiftUI ActionSheet does not dismiss when timer is running

I have the following simple SwiftUI setup. A timer that is running and updating a Text. If the timer is not running (stopped or paused) I can easily show an ActionSheet (by tapping on Actions) and dismiss it by choosing either "Cancel" or "Action 1" option. But if the timer is running, I have a really hard time dismissing the ActionSheet by choosing one of the "Cancel" or "Action 1" options. Do you know what's going on?
I am using Xcode 11.5.
import SwiftUI
struct ContentView: View {
#ObservedObject var stopWatch = StopWatch()
#State private var showActionSheet: Bool = false
var body: some View {
VStack {
Text("\(stopWatch.secondsElapsed)")
HStack {
if stopWatch.mode == .stopped {
Button(action: { self.stopWatch.start() }) {
Text("Start")
}
} else if stopWatch.mode == .paused {
Button(action: { self.stopWatch.start() }) {
Text("Resume")
}
} else if stopWatch.mode == .running {
Button(action: { self.stopWatch.pause() }) {
Text("Pause")
}
}
Button(action: { self.stopWatch.stop() }) {
Text("Reset")
}
}
Button(action: { self.showActionSheet = true }) {
Text("Actions")
}
.actionSheet(isPresented: $showActionSheet) {
ActionSheet(title: Text("Actions"), message: nil, buttons: [.default(Text("Action 1")), .cancel()])
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
class StopWatch: ObservableObject {
#Published var secondsElapsed: TimeInterval = 0.0
#Published var mode: stopWatchMode = .stopped
var timer = Timer()
func start() {
mode = .running
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
self.secondsElapsed += 0.1
}
}
func stop() {
timer.invalidate()
secondsElapsed = 0
mode = .stopped
}
func pause() {
timer.invalidate()
mode = .paused
}
enum stopWatchMode {
case running
case stopped
case paused
}
}
Works fine with Xcode 12 / iOS 14, but try to separate button with sheet into another subview to avoid recreate it on timer counter refresh.
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#ObservedObject var stopWatch = StopWatch()
// #StateObject var stopWatch = StopWatch() // << used for SwiftUI 2.0
#State private var showActionSheet: Bool = false
var body: some View {
VStack {
Text("\(stopWatch.secondsElapsed)")
HStack {
if stopWatch.mode == .stopped {
Button(action: { self.stopWatch.start() }) {
Text("Start")
}
} else if stopWatch.mode == .paused {
Button(action: { self.stopWatch.start() }) {
Text("Resume")
}
} else if stopWatch.mode == .running {
Button(action: { self.stopWatch.pause() }) {
Text("Pause")
}
}
Button(action: { self.stopWatch.stop() }) {
Text("Reset")
}
}
ActionsSubView(showActionSheet: $showActionSheet)
}
}
}
struct ActionsSubView: View {
#Binding var showActionSheet: Bool
var body: some View {
Button(action: { self.showActionSheet = true }) {
Text("Actions")
}
.actionSheet(isPresented: $showActionSheet) {
ActionSheet(title: Text("Actions"), message: nil, buttons: [.default(Text("Action 1")), .cancel()])
}
}
}

Resources