crownSequencer equivalent in SwiftUI - swiftui

I've historically used the WKInterfaceController's property called "crownSequencer" to listen to rotations in the crown (see code below). I can grab the "rotationalValue" of the crown's rotation with the function 'crownDidRotate' as shown below.
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
override func awake(withContext context: Any?) {
// Configure interface objects here.
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
}
override func didAppear() {
super.didAppear()
crownSequencer.delegate = self
crownSequencer.focus()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
}
}
extension InterfaceController : WKCrownDelegate {
func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) {
print(rotationalDelta)
}
}
In SwiftUI, I can instead use the following code to listen to the crown within a View:
import SwiftUI
import SpriteKit
struct GameView: View {
#State var scrollAmount: Float = 0.0
// ...other code to initialise variables used
var body: some View {
SpriteView(scene: skScene!)
.focusable(true)
.digitalCrownRotation($scrollAmount, from: -70, through: 70, by: 0.1, sensitivity: .high, isContinuous: false, isHapticFeedbackEnabled: false)
.onChange(of: scrollAmount) { newValue in
//function )
}
}
This code listens to rotations and binds the "scrollAmount" to a state variable.
However, I can't seem to grab an equivalent of the "rotationalDelta" from the original crownSequencer methodology. Is there a way to grab a "rotationalDelta" in SwiftUI (i.e, grab the change in rotation as opposed to the scrollAmount, which reflects the current level of rotation).

Related

Subscribe SwiftUI View to every update of a combine publisher

Here's a simplified example of an approach I want to take, but I can't get the simple example to work.
I have a Combine publisher who's subject is a view model State:
struct State {
let a: Bool
let b: Bool
let transition: Transition?
}
The State includes a transition property. This describes the Transition that the State made in order to become the current state.
enum Transition {
case onAChange, onBChange
}
I want to use transition property to drive animations in a View subscribed to the publisher so that different transitions animate in specific ways.
View code
Here's the code for the view. You can see how it tries to use the transition to choose an animation to update with.
struct TestView: View {
let model: TestViewModel
#State private var state: TestViewModel.State
private var cancel: AnyCancellable?
init(model: TestViewModel) {
self.model = model
self._state = State(initialValue: model.state.value)
self.cancel = model.state.sink(receiveValue: updateState(state:))
}
var body: some View {
VStack(spacing: 20) {
Text("AAAAAAA").scaleEffect(state.a ? 2 : 1)
Text("BBBBBBB").scaleEffect(state.b ? 2 : 1)
}
.onTapGesture {
model.invert()
}
}
private func updateState(state: TestViewModel.State) {
withAnimation(animation(for: state.transition)) {
self.state = state
}
}
private func animation(for transition: TestViewModel.Transition?) -> Animation? {
guard let transition = transition else { return nil }
switch transition {
case .onAChange: return .easeInOut(duration: 1)
case .onBChange: return .easeInOut(duration: 2)
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView(model: TestViewModel())
}
}
Model code
final class TestViewModel: ObservableObject {
var state = CurrentValueSubject<State, Never>(State(a: false, b: false, transition: nil))
struct State {
let a: Bool
let b: Bool
let transition: Transition?
}
enum Transition {
case onAChange, onBChange
}
func invert() {
let oldState = state.value
setState(newState: .init(a: !oldState.a, b: oldState.b, transition: .onAChange))
setState(newState: .init(a: !oldState.a, b: !oldState.b, transition: .onBChange))
}
private func setState(newState: State) {
state.value = newState
}
}
You can see in the model code that when invert() is called, two state changes occur. The model first toggles a using the .onAChange transition, and then toggles b using the .onBChange transition.
What should happen
What should happen when this is run is that each time the view is clicked, the text "AAAAAAA" and "BBBBBBB" should toggle size. However, the "AAAAAAA" text should change quickly (1 second) and the "BBBBBBB" text should change slowly (2 seconds).
What actually happens
However, when I run this and click on the view, the view doesn't update at all.
I can see from the debugger that onTapGesture { … } is called and invert() is being called on the model. Also updateState(state:) is also being called. However, TestView is not changing on screen, and body is not invoked again.
Other things I tried
Using a callback
Instead of using a publisher to send the event to the view, I've tried a callback function in the model set to the view's updateState(state:) function. I assigned to this in the init of the view with model.handleUpdate = self.update(state:). Again, this did not work. The function invert() and update(state:) were called, as expected, but the view didn't actually change.
Using #ObservedObject
I change the model to be ObservableObject with its state being #Published. I set up the view to have an #ObservedOject for the model. With this, the view does update, but it updates both pieces of text using the same animation, which I don't want. It seems that the two state updates are squashed and it only sees the last one, and uses the transition from that.
Something that did work – sort of
Finally, I tried to directly copy the model's invert() function in to the view's onTapGesture handler, so that the view updates its own state directly. This did work! Which is something, but I don't want to put all by model update logic in my view.
Question
How can I have a SwiftUI view subscribe to all states that a model sends through its publisher so that it can use a transition property in the state to control the animation used for that state change?
The way you subscribe a view to the publisher is by using .onRecieve(_:perform:), so instead of saving a cancellable inside init, do this:
var body: some View {
VStack(spacing: 20) {
Text("AAAAAAA").scaleEffect(state.a ? 2 : 1)
Text("BBBBBBB").scaleEffect(state.b ? 2 : 1)
}
.onTapGesture {
model.invert()
}
.onReceive(model.state, perform: updateState(state:)) // <- here
}

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

AVSpeechSynthesizerDelegate implementation in SwiftUI

Can anyone share how we can implement AVSpeechSynthesizerDelegate in SwiftUI.
how we can listen to delegate callbacks methods in SwiftUI app.
Thanks
One solution would be to define a class which conforms to ObservableObject. The idea would be to use an #Published property to enable SwiftUI to make updates to your UI. Here's an example of a simple way to keep track of the state of an AVSpeechSynthesizer (I'm unsure of your actual use case):
final class Speaker: NSObject, ObservableObject {
#Published var state: State = .inactive
enum State: String {
case inactive, speaking, paused
}
override init() {
super.init()
synth.delegate = self
}
func speak(words: String) {
synth.speak(.init(string: words))
}
private let synth: AVSpeechSynthesizer = .init()
}
Then, make this class conform to AVSpeechSynthesizerDelegate like so:
extension Speaker: AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
self.state = .speaking
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) {
self.state = .paused
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
self.state = .inactive
}
// etc...
}
Here, I've simply used the delegate callbacks to update a single #Published property, but you could update however you like here depending on your use case. The main point to bear in mind with ObservableObjects is using the #Published property wrapper for any properties you wish to drive UI updates upon a change in value. Here's an example view:
struct MyView: View {
#ObservedObject var speaker: Speaker
var body: some View {
// 1
Text("State = \(speaker.state.rawValue)")
.onReceive(speaker.$state) { state in
// 2
}
}
}
Note how there's two ways to use #Published properties in SwiftUI Views. 1: Simply read the value. SwiftUI will update your view upon a value change. 2: Access the #Published property's publisher with the $ prefix. Using Views onReceive method, you can execute code whenever the publisher emits a value.

How to detect Light\Dark mode change in iOS 13?

Some of the UI setups not working automatically with the Dark/Light mode change as the UIColor. For example shadow in layer. As I need to remove and drop shadow in dark and light mode, I need somewhere to put updateShadowIfNeeded() function. I know how to detect what is the mode currently:
func dropShadowIfNeeded() {
switch traitCollection.userInterfaceStyle {
case .dark: removeShadow()
case .light: dropShadowIfNotDroppedYet()
default: assertionFailure("Unknown userInterfaceStyle")
}
}
Now I put the function inside the layoutSubviews, since it gets called every time appearance change:
override func layoutSubviews() {
super.layoutSubviews()
dropShadowIfNeeded()
}
But this function is getting called A LOT. What is the proper function to trigger only if userInterfaceStyle changed?
SwiftUI
With a simple environment variable on the \.colorScheme key:
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
var body: some View {
Text(colorScheme == .dark ? "Its Dark" : "Its. not dark! (Light)")
}
}
UIKit
As it described in WWDC 2019 - Session 214 around 23:30.
As I expected, this function is getting called a lot including when colors changing. Along side with many other functions for ViewController and presentationController. But there is some especial function designed for that has a similar signature in all View representers.
Take a look at this image from that session:
Gray: Calling but not good for my issue, Green: Designed for this
So I should call it and check it inside this function:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
dropShadowIfNeeded()
}
}
This will guarantee to be called just once per change.
if you are only looking for the initial state of the style, check out this answer here
I think this should get called significantly less often, plus the guard makes sure you only react to user interface style changes:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle else {
return
}
dropShadowIfNeeded()
}
With RxSwift and ObjectiveC runtime, you can achieve it without inheritance
here is the encapsulated version:
import UIKit
import RxSwift
import RxCocoa
enum SystemTheme {
static func get(on view: UIView) -> UIUserInterfaceStyle {
view.traitCollection.userInterfaceStyle
}
static func observe(on view: UIView) -> Observable<UIUserInterfaceStyle> {
view.rx.methodInvoked(#selector(UIView.traitCollectionDidChange(_:)))
.map { _ in SystemTheme.get(on: view) }
.distinctUntilChanged()
}
}

In SwiftUI, how to react to changes on "#Published vars" *outside* of a "View"

Suppose I have the following ObservableObject, which generates a random String every second:
import SwiftUI
class SomeObservable: ObservableObject {
#Published var information: String = ""
init() {
Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(updateInformation),
userInfo: nil,
repeats: true
).fire()
}
#objc func updateInformation() {
information = String("RANDOM_INFO".shuffled().prefix(5))
}
}
And a View, which observes that:
struct SomeView: View {
#ObservedObject var observable: SomeObservable
var body: some View {
Text(observable.information)
}
}
The above will work as expected.
The View redraws itself when the ObservableObject changes:
Now for the question:
How could I do the same (say calling a function) in a "pure" struct that also observes the same ObservableObject? By "pure" I mean something that does not conform to View:
struct SomeStruct {
#ObservedObject var observable: SomeObservable
// How to call this function when "observable" changes?
func doSomethingWhenObservableChanges() {
print("Triggered!")
}
}
(It could also be a class, as long as it's able to react to the changes on the observable.)
It seems to be conceptually very easy, but I'm clearly missing something.
(Note: I'm using Xcode 11, beta 6.)
Update (for future readers) (paste in a Playground)
Here is a possible solution, based on the awesome answer provided by #Fabian:
import SwiftUI
import Combine
import PlaygroundSupport
class SomeObservable: ObservableObject {
#Published var information: String = "" // Will be automagically consumed by `Views`.
let updatePublisher = PassthroughSubject<Void, Never>() // Can be consumed by other classes / objects.
// Added here only to test the whole thing.
var someObserverClass: SomeObserverClass?
init() {
// Randomly change the information each second.
Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(updateInformation),
userInfo: nil,
repeats: true
).fire() }
#objc func updateInformation() {
// For testing purposes only.
if someObserverClass == nil { someObserverClass = SomeObserverClass(observable: self) }
// `Views` will detect this right away.
information = String("RANDOM_INFO".shuffled().prefix(5))
// "Manually" sending updates, so other classes / objects can be notified.
updatePublisher.send()
}
}
class SomeObserverClass {
#ObservedObject var observable: SomeObservable
// More on AnyCancellable on: apple-reference-documentation://hs-NDfw7su
var cancellable: AnyCancellable?
init(observable: SomeObservable) {
self.observable = observable
// `sink`: Attaches a subscriber with closure-based behavior.
cancellable = observable.updatePublisher
.print() // Prints all publishing events.
.sink(receiveValue: { [weak self] _ in
guard let self = self else { return }
self.doSomethingWhenObservableChanges()
})
}
func doSomethingWhenObservableChanges() {
print(observable.information)
}
}
let observable = SomeObservable()
struct SomeObserverView: View {
#ObservedObject var observable: SomeObservable
var body: some View {
Text(observable.information)
}
}
PlaygroundPage.current.setLiveView(SomeObserverView(observable: observable))
Result
(Note: it's necessary to run the app in order to check the console output.)
The old way was to use callbacks which you registered. The newer method is to use the Combine framework to create publishers for which you can registers further processing, or in this case a sink which gets called every time the source publisher sends a message. The publisher here sends nothing and so is of type <Void, Never>.
Timer publisher
To get a publisher from a timer can be done directly through Combine or creating a generic publisher through PassthroughSubject<Void, Never>(), registering for messages and sending them in the timer-callback via publisher.send(). The example has both variants.
ObjectWillChange Publisher
Every ObservableObject does have an .objectWillChange publisher for which you can register a sink the same as you do for Timer publishers. It should get called every time you call it or every time a #Published variable changes. Note however, that is being called before, and not after the change. (DispatchQueue.main.async{} inside the sink to react after the change is complete).
Registering
Every sink call creates an AnyCancellable which has to be stored, usually in the object with the same lifetime the sink should have. Once the cancellable is deconstructed (or .cancel() on it is called) the sink does not get called again.
import SwiftUI
import Combine
struct ReceiveOutsideView: View {
#if swift(>=5.3)
#StateObject var observable: SomeObservable = SomeObservable()
#else
#ObservedObject var observable: SomeObservable = SomeObservable()
#endif
var body: some View {
Text(observable.information)
.onReceive(observable.publisher) {
print("Updated from Timer.publish")
}
.onReceive(observable.updatePublisher) {
print("Updated from updateInformation()")
}
}
}
class SomeObservable: ObservableObject {
#Published var information: String = ""
var publisher: AnyPublisher<Void, Never>! = nil
init() {
publisher = Timer.publish(every: 1.0, on: RunLoop.main, in: .common).autoconnect().map{_ in
print("Updating information")
//self.information = String("RANDOM_INFO".shuffled().prefix(5))
}.eraseToAnyPublisher()
Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(updateInformation),
userInfo: nil,
repeats: true
).fire()
}
let updatePublisher = PassthroughSubject<Void, Never>()
#objc func updateInformation() {
information = String("RANDOM_INFO".shuffled().prefix(5))
updatePublisher.send()
}
}
class SomeClass {
#ObservedObject var observable: SomeObservable
var cancellable: AnyCancellable?
init(observable: SomeObservable) {
self.observable = observable
cancellable = observable.publisher.sink{ [weak self] in
guard let self = self else {
return
}
self.doSomethingWhenObservableChanges() // Must be a class to access self here.
}
}
// How to call this function when "observable" changes?
func doSomethingWhenObservableChanges() {
print("Triggered!")
}
}
Note here that if no sink or receiver at the end of the pipeline is registered, the value will be lost. For example creating PassthroughSubject<T, Never>, immediately sending a value and aftererwards returning the publisher makes the messages sent get lost, despite you registering a sink on that subject afterwards. The usual workaround is to wrap the subject creation and message sending inside a Deferred {} block, which only creates everything within, once a sink got registered.
A commenter notes that ReceiveOutsideView.observable is owned by ReceiveOutsideView, because observable is created inside and directly assigned. On reinitialization a new instance of observable will be created. This can be prevented by use of #StateObject instead of #ObservableObject in this instance.