I have been trying to execute getData() after the completion of login(), but that never seems to work when using DispatchQueue.main.async
I have only been able to get it to work by forcing a delay, what's the best way to go about this?
Thanks
class DataTask {
func login{...}
func getData{...}
}
struct ContentView: View {
#State private var data = DataTask()
var body: some View{
Text("Hello world")
.onAppear{
data.login()
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0){
func refreshData(){
data.getData()
// update the response into core data
}
}
}
}
}
Never use hard-coded delays to work around an asynchronous task, that's a horrible practice.
To execute a function after the completion of another add a completion handler
class DataTask {
func login(completion: #escaping: () -> Void) {... completion() ...}
func getData {...}
}
...
data.login {
data.getData()
}
Related
I wrote the following to attempt to display a "arrow.down.circle.fill" and then when tapped display a progress view until tasks are done on the main tread. I then want to display the down arrow image again. I noticed it works fine when using a background thread, but then my UI in my main view doesn't update because published changes on background threads aren't allowed.
struct TestAnimation: View {
#State var isLoading: Bool = false
var body: some View {
if !isLoading {
Image(systemName: "arrow.down.circle.fill")
.font(.system(size: 35))
.padding()
.foregroundColor(.blue)
.imageScale(.large)
.onTapGesture {
DoSomthingMainTread()
}
}
else {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
.padding(30)
.scaleEffect(3)
}
}
func DoSomthingMainTread(){
DispatchQueue.global(qos: .background).async {
isLoading = true
DispatchQueue.main.async {
sleep(3) //call function here
}
isLoading = false
}
}
}
Thanks for any help!!
You are doing things on the wrong queues.
DispatchQueue.global(qos: .background).async {
isLoading = true // NO!!!! This needs to be on the main queue.
DispatchQueue.main.async {
sleep(3) // NO!!! This should be off the main queue.
}
isLoading = false // NO!!! This needs to be on the main queue.
}
You must update isLoading on the main queue, because it is a property of a View. And you should not perform slow, blocking work (like sleep(3)) on the main queue, because it prevents the user interface from responding to user actions.
Your DoSomthingMainTread method should look like this:
func DoSomthingMainTread(){
// Already on main queue because onTapGesture's body runs on main queue.
isLoading = true
DispatchQueue.global(qos: .background).async {
// On background queue, so OK to perform slow, blocking work.
sleep(3) //call function here
DispatchQueue.main.async {
// Back on main queue, so OK to update isLoading.
isLoading = false
}
}
}
Well, this problem is often encountered in my daily development and I prefer to use .redacted first, but here I will provide a method base on your method.
First of all, in the view body it's better to use ZStack, so the view will be changed into following code:
var body: some View {
ZStack {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .blue))
.padding(30)
.scaleEffect(3)
.opacity(isLoading ? 1: 0)
Image(systemName: "arrow.down.circle.fill")
.font(.system(size: 35))
.padding()
.foregroundColor(.blue)
.imageScale(.large)
.onTapGesture {
DoSomthingMainTread()
}
.opacity(isLoading ? 0: 1)
}
}
Notice here I use .opacity to decide whether to show the Image or not. And in the DoSomthingMainTread() method, change the code to:
func DoSomthingMainTread(){
isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
isLoading = false
}
}
That's my answer, hope it's useful for you.
As mentioned by others never sleep on the main thread
unless… you use Swift Concurrency, a Task sleeps without blocking the thread
Task { #MainActor in
isLoading = true
try? await Task.sleep(nanoseconds: 3_000_000)
isLoading = false
}
Or if you want to do something in the background and update the loading state on the main thread create an extra function dispatched to the #MainActor
#MainActor func updateLoadingState(_ flag : Bool) {
isLoading = flag
}
and use a detached Task for the background work
Task.detached(priority: .background) {
await updateLoadingState(true)
doSomethingInBackground()
await updateLoadingState(false)
}
I'm currently making use UITabBarController in SwiftUI. Here is the implementation:
struct MyTabView: View {
private var viewControllers: [UIHostingController<AnyView>]
public init( _ views: [AnyView]) {
self.viewControllers = views.map { UIHostingController(rootView:$0) }
}
public var body: some View {
return TabBarController(controllers: viewControllers)
.edgesIgnoringSafeArea(.all)
}
}
struct TabBarController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UITabBarController {
let tabBarController = UITabBarController()
tabBarController.viewControllers = controllers
return tabBarController
}
func updateUIViewController(_ tabBarController: UITabBarController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITabBarControllerDelegate {
var parent: TabBarController
init(_ tabBarController: TabBarController) {
self.parent = tabBarController
}
}
}
Inside of my SwiftUI I have the following:
struct ContentView: View {
var body: some View {
MyTabView([
AnyView(Text("Moo Moo")),
AnyView(MyPage())
])
}
}
struct MyPage:View {
var body:some View {
NavigationView {
VStack {
ForEach((1...10).reversed(), id: \.self) { value -> AnyView in
print("For Each Value Called")
return AnyView(MyView(text: String(value)))
}
}
}
}
}
struct MyView:View {
let text:String
var body:some View {
Text(text).onAppear {
print("On Appear Called - Making Service Call for \(text)")
}
}
}
I have the following questions:
When running this code the On Appear Called - Making Service Call for \(text), is called twice. What would cause this? My expectation is that it is only run once. Should this be occurring?
Is this a SwiftUI bug lurking around or is this expected behaviour?
Yes, your expectation would be correct. However, it looks like a bug.
The problem appear when having content inside NavigationView. If you use .onAppear() on the NavigationView, you will see it called only once. If you use onAppear() on the VStack, it's already twice.
This has reported in this thread aswell
From my view, this behavior is wrong. Maybe report to Apple or ask why
maybe I found a solution:
add on every very first NavigationLink the modifier .isDetailLink(false)
for me it stops the double onAppear calls
I have been struggling with this over and over again, so I think I'm missing something. I need to do math, make a setting, assign a value or any of a host of simple operations in reaction to some user action, such as the example shown here, and SwiftUI is wanting a View where I don't need a view. There's got to be a way around the ViewBuilder's rules. I kind of worked around this by creating an unnecessary view and executing the code I need inside the View's init(), but that seems terribly awkward.
import SwiftUI
struct ContentView: View
{
#State var showStuff = false
var body: some View
{
VStack
{
Toggle(isOn: $showStuff)
{
Text("Label")
}
if showStuff
{
UserDefaults.standard.set(true, forKey: "Something")
}
}
}
}
Way 1 (best):
struct ExecuteCode : View {
init( _ codeToExec: () -> () ) {
codeToExec()
}
var body: some View {
return EmptyView()
}
}
usage:
HStack {
ExecuteCode {
print("SomeView1 was re-drawn!")
}
SomeView1()
}
Way 2:
( my first way is better - you're able to write only simple code here )
Code with let _ = works inside of View!
HStack {
let _ = print("SomeView1 was re-drawn!")
SomeView1()
}
Way 3:
( my first way is better - too difficult code structure; But code doings the same )
HStack {
// here is the magic
{ () -> SomeView1() in
// here is code to execute
print("SomeView1 was re-drawn!")
// here is the magic
return SomeView1()
}
}
Views are actually so-called Function Builders, and the contents of the view body are used as arguments to to the buildBlock function, as mentioned by #Asperi.
An alternative solution if you must run code inside this context is using a closure that returns the desired view:
VStack {
// ... some views ...
{ () -> Text in
// ... any code ...
return Text("some view") }()
// ... some views ...
}
In SwiftUI 2.0, there's a new ViewModifier onChange(of:perform:), that allows you to react to changes in values.
But you can create something similar to that with a neat trick (I forgot where I saw it, so unfortunately I can't leave proper attribution), by extending a Binding with onChange method:
extension Binding {
func onChange(perform action: #escaping (Value, Value) -> Void) -> Self {
.init(
get: { self.wrappedValue },
set: { newValue in
let oldValue = self.wrappedValue
DispatchQueue.main.async { action(newValue, oldValue) }
self.wrappedValue = newValue
})
}
}
You can use it like so:
Toggle(isOn: $showStuff.onChange(perform: { (new, old) in
if new {
UserDefaults.standard.set(true, forKey: "Something")
}
}))
You cannot do what you try to do, because actually every view block inside body is a ViewBuidler.buildBlock function arguments. Ie. you are in function arguments space. I hope you would not expect that expression like
foo(Toggle(), if showStuff { ... } )
would work (assuming foo is func foo(args: View...). But this is what you try to do in body.
So expressions in SwiftUI have to be out of ViewBuilder block (with some exceptions which ViewBuilder itself supports for views).
Here is a solution for your case:
SwiftUI 2.0
struct ContentView: View {
#AppStorage("Something") var showStuff = false
var body: some View {
VStack {
Toggle(isOn: $showStuff) {
Text("Label")
}
}
}
}
SwiftUI 1.0
Find in already solved SwiftUI toggle switches
Note: View.body (excluding some action modifiers) is equivalent of UIView.draw(_ rect:)... you don't store UserDefaults in draw(_ rect:), do you?
I want to listen to notifications when the app goes to the background and comes back. I'm trying to use the NotificationCenter publishers and have the SwiftUI view listen to them.
I can use a few methods to do it and I'm trying to use two of them but the interesting thing is, that although all seem legit when I put the subscriber in the init() method, it just does not work.
I tried to put it on the main thread but still no success.
Does anyone have any idea why?
Here's my code:
struct ContentView: View {
#State var isActive = true
#State var cancellables = Set<AnyCancellable>()
var body: some View {
ZStack {
Image("background")
.resizable()
.scaledToFill()
.edgesIgnoringSafeArea(.all)
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
self.isActive = false
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification), perform: {_ in
self.isActive = true
})
}
init() {
NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)
// .receive(on: RunLoop.main)
.sink(receiveValue: { _ in
print("init")
}
.store(in: &cancellables)
}
}
Strangely the listener in the onReceive modifier works like a charm. In the init() the print("init") never gets called.
#State is not ready yet in init, so it cannot be used for such purposes. The approach can be as follows:
var cancellables = Set<AnyCancellable>()
init() {
NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)
.sink(receiveValue: { _ in
print(">> in init")
})
.store(in: &cancellables)
}
in such defined cancellables you can store all subscribers created in init, but you will not be able to use it later in code, but this approach is good for once defined notification handlers.
Can you use onAppear?
...
...
var body: some View {
... your body code
}.onAppear(perform: loadNotification)
private func loadNotification() {
NotificationCenter.default.publisher(
....
}
See onAppear:
https://developer.apple.com/documentation/swiftui/view/3278614-onappear
It seems to be the replacement for viewDidLoad
I can't implement NFCNDEFReaderSessionDelegate in SwiftUI
So I create a class to implement it
struct ContentView: View {
#State var out="n/a"
var body: some View {
VStack{
Text(out)
Button(
action:{
self.scan()
},
label:{Text("Scan")}
)
}
}
func scan(){
var nfc=Nfc()
nfc.start()
out = ???
}
}
Nfc.swift
class Nfc: NSObject, NFCNDEFReaderSessionDelegate{
func start(){
let session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: true)
session.begin()
print("start")
}
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
print("didInvalidateWithError")
}
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
print("scan")
}
func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
print("readerSessionDidBecomeActive")
}
}
I can successfully scan the NFC tag, but it did not trigger any callback, only show me this error
[CoreNFC] 00000002 81bfda80 -[NFCNDEFReaderSession _callbackDidBecomeActive]:228 Delegate does not implement -readerSessionDidBecomeActive: method
But I actually implemented this method.
If I add this method, I cannot scan the tag.
func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
print("didDetect")
}
You should create a view that will conform to UIViewControllerRepresentable and the view has a Coordinator that handles all the delegate functionalities.
The issue is not related to the SwiftUI implementation, just add the readerSessionDidBecomeActive callback to your Nfc class:
public func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
print("readerSessionDidBecomeActive")
}