Capturing UndoManager from SwiftUI environment - swiftui

I want to be able to access the UndoManager from inside my document model, so I can register undo actions from within the model:
// Assume I've extended MyDocument to conform to ReferenceFileDocument elsewhere...
final class MyDocument {
private var undoManager: UndoManager?
#Published var aNumber = 5 {
willSet {
if let undoManager = undoManager {
let currentValue = self.aNumber
undoManager.registerUndo(withTarget: self) { target in
target.aNumber = currentValue
}
}
}
}
func setUndoManager(undoManager: UndoManager?) {
self.undoManager = undoManager
}
}
To be register the undoManager, I have tried this:
struct DocumentView: View {
let document : MyDocument
#Environment(\.undoManager) var undoManager
var body: some View {
MyDocumentEditor(document: document)
.onAppear {
document.setUndoManager(undoManager: undoManager)
}
}
}
When running my app and loading a saved document this works. But when starting from a new document the UndoManager is nil.
I've tried things like:
#Environment(\.undoManager) var undoManager {
didSet {
self.document.setUndoManager(undoManager: undoManager)
}
}
My objective here is to try and keep as much logic in the model and the views focusing only on UI stuff as much as possible. I wish that ReferenceFileDocument gave a property to access its associated UndoManager as is available with NSDocument.

It looks more natural for SwiftUI to use the following approach
var body: some View {
TopLevelView(document: document, undoManager: undoManager)
}
and
struct TopLevelView: View {
#ObservedObject var document : MyDocument
var undoManager: UndoManager?
init(document: MyDocument, undoManager: UndoManager?) {
self.document = document
self.undoManager = undoManager
self.setUndoManager(undoManager: undoManager)
}
// ... other code
}

I have found a solution to this - although it doesn't feel right. At the top level of the View I pass the undoManager to a property I hold on the document:
struct ContentView: View {
let document: MyDocument
#Environment(\.undoManager) var undoManager
var body: some View {
document.setUndoManager(undoManager: undoManager)
return TopLevelView(document: document)
}
}

After trying to figure this out for more than a day, my takeaway is that the UndoManager in the Environment is the one attached to the NSWindow where the view lives. My solution is:
protocol Undoable {
func inverted() -> Self
}
class Store<State, Action : Undoable> {
var state : State
var reducer : (inout State, Action) -> Void
//...init...
func send(_ action: Action, undoManager: UndoManager) {//passed as an argument
reducer(&state, action)
undoManager.registerUndo(withTarget: self){target in
target.send(action.inverted())
}
}
//...other methods...
}
Store can of course be your document class. Now you can pass the UndoManager found in the environment from any view that sends actions (pay attentions to sheets and alerts though). Or you automate that step away:
class Dispatcher<State, Action : Undoable> : ObservableObject {
let store : Store<State, Action>
let undoManager : UndoManager //see below
//...init...
func send(_ action: Action) {
objectWillChange.send()
store.send(action, undoManager: undoManager)
}
}
struct ContentView<State, Action : Undoable> : View {
#Environment(\.undoManager) var undoManager
let document : Store<State, Action>
var body : some View {
ViewHierarchy().environmentObject(Dispatcher(store: document,
undoManager: undoManager)
}
}
(maybe you'd need to put the Dispatcher into a StateObject, I didn't test that part because I'm happy passing the undo manager as a function argument in my small app).

Related

#Published struct in SwiftUI does not published

See my code below. My problem is if i change accessToken via UserService.shared.currentStore.accessToken = xxx, SwiftUI doesn't publish, and there's no update on StoreBanner at all.
//SceneDelegate
let contentView = ContentView()
.environmentObject(UserService.shared)
//Define
class UserService: ObservableObject {
#Published var currentStore = Store.defaultValues()
static let shared = UserService()
}
struct Store: Codable, Hashable {
var storeName: String = ""
var accessToken: String = ""
}
//Use it
struct StoreBanner: View {
var body: some View {
Group {
if UserService.shared.currentStore.accessToken.isNotEmpty {
ShopifyLinkedBanner()
} else {
ShopifyLinkBanner()
}
}
}
}
You're trying to use UserService inside StoreBanner without using a property wrapper to tell the view to respond to updates. Without the #ObservedObject property wrapper, the View doesn't have a mechanism to know that any of the #Published properties have been updated.
Try this:
struct StoreBanner: View {
#ObservedObject private var userService = UserService.shared
var body: some View {
Group {
if userService.currentStore.accessToken.isNotEmpty {
ShopifyLinkedBanner()
} else {
ShopifyLinkBanner()
}
}
}
}
This should work assuming you set accessToken somewhere in your code on the same instance of UserService.

SwiftUI Combine Why can't bind data in init?

I am trying a very simple test to just combine a simple Just("JustValue") to a property.
But it did not work.
↓ This is my code
struct ResponseView: View {
private var contentCancellable: AnyCancellable? = nil
#State var content: String = "InitialValue"
var body: some View {
Text(content)
}
init() {
contentCancellable = Just("JustValue").assign(to: \.content, on: self)
}
}
Is there anyone know why the Text shows "InitialValue" instead "JustValue"
This is specific of state property wrapper initialization pass... the external state storage is created later so only one initialisation is applied.
If you want to update it, do it later, when state be already created and linked to view, like
struct ResponseView: View {
#State var content: String = "InitialValue"
var body: some View {
Text(content)
.onAppear {
_ = Just("JustValue").assign(to: \.content, on: self)
}
}
}
the gives UI which you expected.

SwiftUI how to delete from ForEach by ID, not index

The following code makes me uncomfortable.
struct HistoryView: View {
#ObservedObject var history: History
var body: some View {
List {
ForEach(history.getSessions()) { sess in
Text("Duration: \(sess.duration)")
}.onDelete(perform: self.onDelete)
}
}
private func onDelete(_ indexSet: IndexSet) {
...
}
}
The problem is that History is a very asynchronous thing. It's a model connected to CloudKit. It could be getting updates in the background. Ideally, if it updates, this View would update immediately and the indexes would still be accurate, but I much prefer to get a set of the identifiers to delete, not set of indexes. Is there a way to do this? Note: those "history sessions" conform to Identifiable as required by ForEach, so they all have IDs. In my case they are UUIDs.
maybe this could help lock history sessions while you delete the index you selected:
struct HistoryView: View {
#ObservedObject var history: History
private let syncQueue = DispatchQueue(label: "com.xxx.yyy.zzz")
var body: some View {
List {
ForEach(history.getSessions()) { sess in
Text("Duration: \(sess.duration)")
}.onDelete(perform: self.onDelete)
}
}
private func onDelete(_ indexSet: IndexSet) {
syncQueue.sync {
// do the delete
}
}
}

SwiftUI get EnvironmentValues of some View given only a reference to the view

According to my understanding, if you define a view yourself (as a struct that implements View), then you can declare some var to be an Environment variable, like this:
#Environment(\.isEnabled) var isEnabled
This will give you access to the EnvironmentValues.isEnabled field.
However, it seems like this is only possible within the view definition itself.
Is it possible, given some view v, to get the environment object of that view? or get specific environment values?
I assume that taking into account that SwiftUI is reactive state managed framework then yes, directly you cannot ask for internal view environment value, actually because you can't be sure when that environment is set up for this specific view. However you can ask that view to tell you about its internal state, when view is definitely knowns about it... via binding.
An example code (a weird a bit, but just for demo) how this can be done see below.
struct SampleView: View {
#Environment(\.isEnabled) private var isEnabled
var myState: Binding<Bool>?
var body: some View {
VStack {
Button(action: {}) { Text("I'm \(isEnabled ? "Enabled" : "Disabled")") }
report()
}
}
func report() -> some View {
DispatchQueue.main.async {
self.myState?.wrappedValue = self.isEnabled
}
return EmptyView()
}
}
struct TestEnvironmentVar: View {
#State private var isDisabled = false
#State private var sampleState = true
var body: some View {
VStack {
Button(action: {
self.isDisabled.toggle()
}) {
Text("Toggle")
}
Divider()
sampleView()
.disabled(isDisabled)
}
}
private func sampleView() -> some View {
VStack {
SampleView(myState: $sampleState)
Text("Sample is \(sampleState ? "Enabled" : "Disabled")")
}
}
}
struct TestEnvironmentVar_Previews: PreviewProvider {
static var previews: some View {
TestEnvironmentVar()
}
}

SwiftUI: How to persist #Published variable using UserDefaults?

I want a #Published variable to be persisted, so that it's the same every time when I relaunch my app.
I want to use both the #UserDefault and #Published property wrappers on one variable. For example I need a '#PublishedUserDefault var isLogedIn'.
I have the following propertyWrapper
import Foundation
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
This is my Settings class
import SwiftUI
import Combine
class Settings: ObservableObject {
#Published var isLogedIn : Bool = false
func doLogin(params:[String:String]) {
Webservice().login(params: params) { response in
if let myresponse = response {
self.login = myresponse.login
}
}
}
}
My View class
struct HomeView : View {
#EnvironmentObject var settings: Settings
var body: some View {
VStack {
if settings.isLogedIn {
Text("Loged in")
} else{
Text("Not Loged in")
}
}
}
}
Is there a way to make a single property wrapper that covers both the persisting and the publishing?
import SwiftUI
import Combine
fileprivate var cancellables = [String : AnyCancellable] ()
public extension Published {
init(wrappedValue defaultValue: Value, key: String) {
let value = UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
self.init(initialValue: value)
cancellables[key] = projectedValue.sink { val in
UserDefaults.standard.set(val, forKey: key)
}
}
}
class Settings: ObservableObject {
#Published(key: "isLogedIn") var isLogedIn = false
...
}
Sample: https://youtu.be/TXdAg_YvBNE
Version for all Codable types check out here
To persist your data you could use the #AppStorage property wrapper.
However, without using #Published your ObservableObject will no longer put out the news about the changed data. To fix this, simply call objectWillChange.send() from the property's willSet observer.
import SwiftUI
class Settings: ObservableObject {
#AppStorage("Example") var example: Bool = false {
willSet {
// Call objectWillChange manually since #AppStorage is not published
objectWillChange.send()
}
}
}
It should be possible to compose a new property wrapper:
Composition was left out of the first revision of this proposal,
because one can manually compose property wrapper types. For example,
the composition #A #B could be implemented as an AB wrapper:
#propertyWrapper
struct AB<Value> {
private var storage: A<B<Value>>
var wrappedValue: Value {
get { storage.wrappedValue.wrappedValue }
set { storage.wrappedValue.wrappedValue = newValue }
}
}
The main benefit of this approach is its predictability: the author of
AB decides how to best achieve the composition of A and B, names it
appropriately, and provides the right API and documentation of its
semantics. On the other hand, having to manually write out each of the
compositions is a lot of boilerplate, particularly for a feature whose
main selling point is the elimination of boilerplate. It is also
unfortunate to have to invent names for each composition---when I try
the compose A and B via #A #B, how do I know to go look for the
manually-composed property wrapper type AB? Or maybe that should be
BA?
Ref: Property WrappersProposal: SE-0258
You currently can't wrap #UserDefault around #Published since that is not currently allowed.
The way to implement #PublishedUserDefault is to pass an objectWillChange into the wrapper and call it before setting the variable.
struct HomeView : View {
#StateObject var auth = Auth()
#AppStorage("username") var username: String = "Anonymous"
var body: some View {
VStack {
if username != "Anonymous" {
Text("Logged in")
} else{
Text("Not Logged in")
}
}
.onAppear(){
auth.login()
}
}
}
import SwiftUI
import Combine
class Auth: ObservableObject {
func login(params:[String:String]) {
Webservice().login(params: params) { response in
if let myresponse = response {
UserDefaults.standard.set(myresponse.login, forKey: "username")`
}
}
}
}