asynchronous initialisation with swiftui - swiftui

Basically - I run up against this a lot - I don't understand how you correctly do asynchronous initialisation in swift with callbacks. (with combine - I can do it). In particular - I have this code:
struct MyView : View {
#State var initialised : Bool = false
init()
{
var initialisedBinding = $initialised
Photos.PHPhotoLibrary.RequestAuthorization {
status in
if (status == Photos.PHAuthorizationStatus.authorized) {
print("here I am")
initialisedBinding.wrappedValue = true
initialisedBinding.update()
}
}
}
var body : some View {
VStack {
if (initialised) {
Text("yep")
} else {
Text("nope")
}
}
}
And when I run it - I get the print out - but the text never changes - it always remains "nope". What am I doing wrong, and how do I do it right? (Without using combine - I can do it with like a currentValueSubject and a .onreceive - but it's extra overhead, and I really want to know why the above code doesn't work - obviously I'm understanding something bad)

State is not ready in init yet, so you bound to nowhere. Moreover such activity in init is not good, because view can be created many times during rendering. The more appropriate place is .onAppear
struct MyView : View {
#State var initialised : Bool = false
var body : some View {
VStack {
if (initialised) {
Text("yep")
} else {
Text("nope")
}
}.onAppear {
Photos.PHPhotoLibrary.RequestAuthorization {
status in
if (status == Photos.PHAuthorizationStatus.authorized) {
print("here I am")
self.initialised = true
}
}
}
}
}

Related

Deleting SwiftUI Items from List Based on Section

I have a TodoListView which displays a List in sections. The sections can be pending or completed as shown below. I am using Realm for my app. The problem is that how can I find out the task I want to delete, since they can belong in different sections.
What should I do in my onDelete function to delete the task in a particular section.
enum Sections: String, CaseIterable {
case pending = "Pending"
case completed = "Completed"
}
struct TodoListView: View {
#ObservedResults(Task.self) var tasks: Results<Task>
var pendingTasks: [Task] {
tasks.filter { $0.isCompleted == false }
}
var completedTasks: [Task] {
tasks.filter { $0.isCompleted == true }
}
var body: some View {
let _ = print(Self._printChanges())
List {
ForEach(Sections.allCases, id: \.self) { section in
Section {
let filteredTasks = section == .pending ? pendingTasks: completedTasks
if filteredTasks.isEmpty {
Text("No tasks")
}
ForEach(filteredTasks, id: \._id) { task in
HStack {
TaskCellView(task: task)
}
}.onDelete { indexSet in
// What to do here:
// can I get the element which I swiped to delete on
}
} header: {
Text(section.rawValue)
}
}
}.listStyle(.plain)
}
}
UPDATE:
I was able to write the following code and it seems to be working fine:
.onDelete { indexSet in
var filteredTasks: [Task] = []
switch section {
case .pending:
filteredTasks = tasks.filter { $0.isCompleted == false }
case .completed:
filteredTasks = tasks.filter { $0.isCompleted == true }
}
indexSet.forEach { index in
let task = filteredTasks[index]
$tasks.remove(task)
}
}

How to conditionally execute code on onAppear method

I have a swiftUi view depending on a class data.
Before displaying the data, I have to compute it in .onAppear method.
I would like to make this heavy computation only when my observed object changes.
The problem is that .onAppear is called every time I open the view, but the object value does not change very often.
Is it possible to conditionally execute the compute function, only when observed data has effectively been modified ?
import SwiftUI
struct test2: View {
#StateObject var person = Person()
#State private var computedValue = 0
var body: some View {
List {
Text("age = \(person.age)")
Text("computedValue = \(computedValue)")
}
.onAppear {
computedValue = compute(person.age) /// Executed much too often :(
}
}
func compute(_ age: Int) -> Int {
return age * 2 /// In real life, heavy computing
}
}
class Person: ObservableObject {
var age: Int = 0
}
Thanks for advice :)
It would probably be a little less code in the view model, but to do all of the calculations in the view, you will need a few changes. First, your class Person has no #Published variables, so it will never call for a state change. I have fixed that.
Second, now that your state will update, you can add an .onReceive() to the view to keep track of when age updates.
Third, and extremely important, to keep from blocking the main thread with the "heavy computing", you should implement Async Await. As a result, even though I sleep the thread for 3 seconds, the UI is still fluid.
struct test2: View {
#StateObject var person = Person()
#State private var computedValue = 0
var body: some View {
List {
Text("age = \(person.age)")
Text("computedValue = \(computedValue)")
Button {
person.age = Int.random(in: 1...80)
} label: {
Text("Randomly Change Age")
}
}
// This will do your initial setup
.onAppear {
Task {
computedValue = await compute(person.age) /// Executed much too often :(
}
}
// This will keep it current
.onReceive(person.objectWillChange) { _ in
Task {
computedValue = await compute(person.age) /// Executed much too often :(
}
}
}
func compute(_ age: Int) async -> Int {
//This is just to simulate heavy work.
do {
try await Task.sleep(nanoseconds: UInt64(3.0 * Double(NSEC_PER_SEC)))
} catch {
//handle error
}
return age * 2 /// In real life, heavy computing
}
}
class Person: ObservableObject {
#Published var age: Int = 0
}
A possible solution is that create a EnvironmentObject with a Bool Value, Change The value of that variable when there are Change in your object.
So onappear just check if environmentobject value is true or false and it will execute you function.
Hope You Found This Useful.
task(id:priority:_:) is the solution for that.
"A view that runs the specified action asynchronously when the view appears, or restarts the task with the id value changes."
Set the id param to the data you want to monitor for changes.

Initialize optional #AppStorage property with non-nil value

I need an optional #AppStorage String property (for a NavigationLink selection, which required optional), so I declared
#AppStorage("navItemSelected") var navItemSelected: String?
I need it to start with a default value that's non-nil, so I tried:
#AppStorage("navItemSelected") var navItemSelected: String? = "default"
but that doesn't compile.
I also tried:
init() {
if navItemSelected == nil { navItemSelected = "default" }
}
But this just overwrites the actual persisted value whenever the app starts.
Is there a way to start it with a default non-nil value and then have it persisted as normal?
Here is a simple demo of possible approach based on inline Binding (follow-up of my comment above).
Tested with Xcode 13 / iOS 15
struct DemoAppStoreNavigation: View {
static let defaultNav = "default"
#AppStorage("navItemSelected") var navItemSelected = Self.defaultNav
var body: some View {
NavigationView {
Button("Go Next") {
navItemSelected = "next"
}.background(
NavigationLink(isActive: Binding(
get: { navItemSelected != Self.defaultNav },
set: { _ in }
), destination: {
Button("Return") {
navItemSelected = Self.defaultNav
}
.onDisappear {
navItemSelected = Self.defaultNav // << for the case of `<Back`
}
}) { EmptyView() }
)
}
}
}
#AppStorage is a wrapper for UserDefaults, so you can simply register a default the old-fashioned way:
UserDefaults.standard.register(defaults: ["navItemSelected" : "default"])
You will need to call register(defaults:) before your view loads, so I’d recommend calling it in your App’s init or in application(_:didFinishLaunchingWithOptions:).

SwiftUI - ReferenceFileDocument - Inability to indicate a document needs saving

When creating a class conforming to ReferenceFileDocument, how do you indicate the document needs saving. i.e. the equivalent of the NSDocument's updateChangeCount method?
I've met the same problem that the SwiftUI ReferenceFileDocument cannot trigger the update. Recently, I've received feedback via the bug report and been suggested to register an undo.
Turns out the update of ReferenceFileDocument can be triggered, just like UIDocument, by registering an undo action. The difference is that the DocumentGroup explicitly implicitly setup the UndoManager via the environment.
For example,
#main
struct RefDocApp: App {
var body: some Scene {
DocumentGroup(newDocument: {
RefDocDocument()
}) { file in
ContentView(document: file.document)
}
}
}
struct ContentView: View {
#Environment(\.undoManager) var undoManager
#ObservedObject var document: RefDocDocument
var body: some View {
TextEditor(text: Binding(get: {
document.text
}, set: {
document.text = $0
undoManager?.registerUndo(withTarget: document, handler: {
print($0, "undo")
})
}))
}
}
I assume at this stage, the FileDocument is actually, on iOS side, a wrapper on top of the UIDocument, the DocumentGroup scene explicitly implicitly assign the undoManager to the environment. Therefore, the update mechanism is the same.
The ReferenceFileDocument is ObservableObject, so you can add any trackable or published property for that purpose. Here is a demo of possible approach.
import UniformTypeIdentifiers
class MyTextDocument: ReferenceFileDocument {
static var readableContentTypes: [UTType] { [UTType.plainText] }
func snapshot(contentType: UTType) throws -> String {
defer {
self.modified = false
}
return self.storage
}
#Published var modified = false
#Published var storage: String = "" {
didSet {
self.modified = true
}
}
}
ReferenceFileDocument exists for fine grained controll over the document. In comparison, a FileDocument has to obey value semantics which makes it very easy for SwiftUI to implement the undo / redo functionality as it only needs to make a copy before each mutation of the document.
As per the documentation of the related DocumentGroup initializers, the undo functionality is not provided automatically. The DocumentGroup will inject an instance of an UndoManger into the environment which we can make use of.
However an undo manager is not the only way to update the state of the document. Per this documentation AppKit and UIKit both have the updateChangeCount method on their native implementation of the UI/NSDocument object. We can reach this method by grabbing the shared document controller on macOS from within the view and finding our document. Unfortunately I don't have a simple solution for the iOS side. There is a private SwiftUI.DocumentHostingController type which holds a reference to our document, but that would require mirroring into the private type to obtain the reference to the native document, which isn't safe.
Here is a full example:
import SwiftUI
import UniformTypeIdentifiers
// DOCUMENT EXAMPLE
extension UTType {
static var exampleText: UTType {
UTType(importedAs: "com.example.plain-text")
}
}
final class MyDocument: ReferenceFileDocument {
// We add `Published` for automatic SwiftUI updates as
// `ReferenceFileDocument` refines `ObservableObject`.
#Published
var number: Int
static var readableContentTypes: [UTType] { [.exampleText] }
init(number: Int = 42) {
self.number = number
}
init(configuration: ReadConfiguration) throws {
guard
let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8),
let number = Int(string)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.number = number
}
func snapshot(contentType: UTType) throws -> String {
"\(number)"
}
func fileWrapper(
snapshot: String,
configuration: WriteConfiguration
) throws -> FileWrapper {
// For the sake of the example this force unwrapping is considered as safe.
let data = snapshot.data(using: .utf8)!
return FileWrapper(regularFileWithContents: data)
}
}
// APP EXAMPLE FOR MACOS
#main
struct MyApp: App {
var body: some Scene {
DocumentGroup.init(
newDocument: {
MyDocument()
},
editor: { file in
ContentView(document: file.document)
.frame(width: 400, height: 400)
}
)
}
}
struct ContentView: View {
#Environment(\.undoManager)
var _undoManager: UndoManager?
#ObservedObject
var document: MyDocument
var body: some View {
VStack {
Text(String("\(document.number)"))
Button("randomize") {
if let undoManager = _undoManager {
let currentNumber = document.number
undoManager.registerUndo(withTarget: document) { document in
document.number = currentNumber
}
}
document.number = Int.random(in: 0 ... 100)
}
Button("randomize without undo") {
document.number = Int.random(in: 0 ... 100)
// Let the system know that we edited the document, which will
// eventually trigger the auto saving process.
//
// There is no simple way to mimic this on `iOS` or `iPadOS`.
let controller = NSDocumentController.shared
if let document = controller.currentDocument {
// On `iOS / iPadOS` change the argument to `.done`.
document.updateChangeCount(.changeDone)
}
}
}
}
}
Unfortunatelly SwiftUI (v2 at this moment) does not provide a native way to mimic the same functionality, but this workaround is still doable and fairly consice.
Here is a gist where I extended the example with a custom DocumentReader view and a DocumentProxy which can be extended for common document related operations for more convenience: https://gist.github.com/DevAndArtist/eb7e8aa5e7134610c20b1a7aca358604

put observedObject in List

I get the data from my api and create a class for them. I can use swifyJSON to init them correctly. The problem is that when I put my observedObject in a List, it can only show correctly once. It will crashed after I changed the view. It's very strong because my other List with similar data struct can work.(this view is in a tabView) Is somebody know where my getAllNotification() should put view.onAppear() or List.onAppear()? Thanks!!
class ManagerNotification : Identifiable, ObservableObject{
#Published var id = UUID()
var notifyId : Int = 0
var requestId : Int = 0
var requestName: String = ""
var groupName : String = ""
// var imageName: String { return name }
init(jsonData:JSON) {
notifyId = jsonData["notifyId"].intValue
requestId = jsonData["requestId"].intValue
requestName = jsonData["requestName"].stringValue
groupName = jsonData["groupName"].stringValue
}
}
import SwiftUI
import SwiftyJSON
struct NotificationView: View {
var roles = ["userNotification", "managerNotification"]
#EnvironmentObject var userToken:UserToken
#State var show = false
#State private var selectedIndex = 0
#State var userNotifications : [UserNotification] = [UserNotification]()
#State var managerNotifications : [ManagerNotification] = [ManagerNotification]()
var body: some View {
VStack {
Picker(selection: $selectedIndex, label: Text(" ")) {
ForEach(0..<roles.count) { (index) in
Text(self.roles[index])
}
}
.pickerStyle(SegmentedPickerStyle())
containedView()
Spacer()
}
.onAppear(perform: getAllNotification)
}
func containedView() -> AnyView {
switch selectedIndex {
case 0:
return AnyView(
List(userNotifications) { userNotification in
UserNotificationCellView(userNotification: userNotification)
}
)
case 1:
return AnyView(
List(managerNotifications) { managernotification in
ManagerNotificationCellView(managerNotification : managernotification)
}
.onAppear(perform: getManagerNotification)
)
default:
return AnyView(Text("22").padding(40))
}
}
func getAllNotification(){
// if (self.userNotifications.count != 0){
// self.userNotifications.removeAll()
// }
// I think the crash was in here, because when i don't use removeAll().
// It works fine, but i don't want every times i change to this view. my array will be longer and
// longer
if (self.managerNotifications.count != 0){
self.managerNotifications.removeAll()
}
NetWorkController.sharedInstance.connectApiByPost(api: "/User/email", params: ["token": "\(self.userToken.token)"])
{(jsonData) in
if let result = jsonData["msg"].string{
print("eeee: \(result)")
if(result == "you dont have any email"){
}else if(result == "success get email"){
if let searchResults = jsonData["mail"].array {
for notification in searchResults {
self.userNotifications.append(UserNotification(jsonData: notification))
}
}
}
}
}
NetWorkController.sharedInstance.connectApiByPost(api: "/Manager/email", params: ["token": "\(self.userToken.token)"])
{(jsonData) in
if let result = jsonData["msg"].string{
print("eeee: \(result)")
if(result == "you dont have any email"){
}else if(result == "success get email"){
if let searchResults = jsonData["mail"].array {
for notification in searchResults {
self.managerNotifications.append(ManagerNotification(jsonData: notification))
}
}
}
}
}
}
func getManagerNotification(){
// if (self.managerNotifications.count != 0){
// self.managerNotifications.removeAll()
// }
print(self.managerNotifications.count)
NetWorkController.sharedInstance.connectApiByPost(api: "/Manager/email", params: ["token": "\(self.userToken.token)"])
{(jsonData) in
if let result = jsonData["msg"].string{
print("eeee: \(result)")
if(result == "you dont have any email"){
}else if(result == "success get email"){
if let searchResults = jsonData["mail"].array {
for notification in searchResults {
self.managerNotifications.append(ManagerNotification(jsonData: notification))
}
}
}
}
}
}
}
error message
Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. reason: 'attempt to delete section 0, but there are only 0 sections before the update'
I think you are confused about the role of #State and #ObservebableObject; it's not like MVC where you replace the ViewController with a SwiftUI.View as it appears you are trying to do in your example. Instead the view should be a function of either some local #State and/or an external #ObservedObject. This is closer to MVVM where your #ObservedObject is analogous to the ViewModel and the view will rebuild itself in response to changes in the #Published properties on the ObservableObject.
TLDR: move your fetching logic to an ObservableObject and use #Published to allow the view to subscribe to the results. I have an example here: https://github.com/joshuajhomann/TVMaze-SwiftUI-Navigation