I'm experiencing this really weird issue/bug with SwiftUI. In the setupSubscription method, I'm creating a subscription to subject and inserting it into the cancellables Set. And yet, when I print the count of cancellables, I get zero. How can the set be empty if I just inserted an element into it?
This is presumably why the handleValue method is not called when I tap on the button. Here's the full output from the console:
init
begin setupSubscription
setupSubscription subject sink: receive subscription: (CurrentValueSubject)
setupSubscription subject sink: request unlimited
setupSubscription subject sink: receive value: (initial value)
handleValue: 'initial value'
setupSubscription: cancellables.count: 0
setupSubscription subject sink: receive cancel
sent value: 'value 38'
cancellables.count: 0
sent value: 'value 73'
cancellables.count: 0
sent value: 'value 30'
cancellables.count: 0
What am I doing wrong? why Is my subscription to subject getting cancelled? Why is handleValue not getting called when I tap the button?
import SwiftUI
import Combine
struct Test: View {
#State private var cancellables: Set<AnyCancellable> = []
let subject = CurrentValueSubject<String, Never>("initial value")
init() {
print("init")
self.setupSubscription()
}
var body: some View {
VStack {
Button(action: {
let newValue = "value \(Int.random(in: 0...100))"
self.subject.send(newValue)
print("sent value: '\(newValue)'")
print("cancellables.count:", cancellables.count)
}, label: {
Text("Tap Me")
})
}
}
func setupSubscription() {
print("begin setupSubscription")
let cancellable = self.subject
.print("setupSubscription subject sink")
.sink(receiveValue: handleValue(_:))
self.cancellables.insert(cancellable)
print("setupSubscription: cancellables.count:", cancellables.count)
// prints "setupSubscription: cancellables.count: 0"
}
func handleValue(_ value: String) {
print("handleValue: '\(value)'")
}
}
a few things you are doing wrong here.
Never try to store things in swiftUI structs. They get invalidated and reloaded every time your view changes. This is likely why your subscription is getting canceled.
For something like this, you should use an ObservableObject or StateObject with published properties. When ObservableObjects or StateObjects change. The views that contain them reload just like with #State or #Binding:
// ObservedObjects have an implied objectWillChange publisher that causes swiftUI views to reload any time a published property changes. In essence they act like State or Binding variables.
class ViewModel: ObservableObject {
// Published properties ARE combine publishers
#Published var subject: String = "initial value"
}
then in your view:
#ObservedObject var viewModel: ViewModel = ViewModel()
If you do need to use a publisher. Or if you need to do something when an observable object property changes. You don't need to use .sink. That is mostly used for UIKit apps using combine. SwiftUI has an .onReceive viewmodifier that does the same thing.
Here are my above suggestions put into practice:
struct Test: View {
class ViewModel: ObservedObject {
#Published var subject: String = "initial value"
}
#ObservedObject var viewModel: Self.ViewModel
var body: some View {
VStack {
Text("\(viewModel.subject)")
Button {
viewModel.subject = "value \(Int.random(in: 0...100))"
} label: {
Text("Tap Me")
}
}
.onReceive(viewModel.$subject) { [self] newValue in
handleValue(newValue)
}
}
func handleValue(_ value: String) {
print("handleValue: '\(value)'")
}
}
You just incorrectly use state - it is view related and it becomes available (prepared back-store) only when view is rendered (ie. in body context). In init there is not yet state back-storage, so your cancellable just gone.
Here is possible working approach (however I'd recommend to move everything subject related into separated view model)
Tested with Xcode 12 / iOS 14
struct Test: View {
private var cancellable: AnyCancellable?
private let subject = CurrentValueSubject<String, Never>("initial value")
init() {
cancellable = self.subject
.print("setupSubscription subject sink")
.sink(receiveValue: handleValue(_:))
}
var body: some View {
VStack {
Button(action: {
let newValue = "value \(Int.random(in: 0...100))"
self.subject.send(newValue)
print("sent value: '\(newValue)'")
}, label: {
Text("Tap Me")
})
}
}
func handleValue(_ value: String) {
print("handleValue: '\(value)'")
}
}
Related
I have my data stored in an Actor. I need to display the data on a view. The only way I have found to do this is to spin up a task to load the data by using an await command on the actor. This doesn't feel right as it is very clunky; it is also giving me an error which I don't understand.
Mutable capture of 'inout' parameter 'self' is not allowed in concurrently-executing code
This is my code:
actor SimpleActor
{
func getString() -> String
{
return "some value"
}
}
struct SimpleView: View
{
var actor: SimpleActor
#State var value: String
init()
{
actor = SimpleActor()
Task
{
value = await actor.getString() // error here
}
}
var body: some View
{
Text(value)
.width(100, alignment: .leading)
}
}
What is the best way of doing this?
There are a lot of issues in the code. For example you cannot modify a simple property without marked as #State(Object). And you cannot initialize a property (without default value) inside a Task.
Basically declare the actor as #StateObject and adopt ObservableObject
Further get the value in the .task modifier and give value a default value
actor SimpleActor : ObservableObject
{
func getString() -> String
{
return "some value"
}
}
struct DetailView: View
{
#StateObject var actor = SimpleActor()
#State private var value = ""
var body: some View
{
Group {
Text(value)
}.task {
value = await actor.getString()
}
}
}
Something close to this would be a common way of doing what you want.
actor SimpleActor: ObservableObject {
func getString() -> String {
return "some value"
}
}
struct SimpleView: View {
#StateObject var actor = SimpleActor()
#State var value: String = ""
var body: some View {
Text(value)
.frame(width: 100, alignment: .leading)
.task {
value = await actor.getString()
}
}
}
.task is iOS 15+, so could go back to .onAppear if you need iOS 13+.
I have a parent state that might exist:
class Model: ObservableObject {
#Published var name: String? = nil
}
If that state exists, I want to show a child view. In this example, showing name.
If name is visible, I'd like it to be shown and editable. I'd like this to be two-way editable, that means if Model.name changes, I'd like it to push to the ChildUI, if the ChildUI edits this, I'd like it to reflect back to Model.name.
However, if Model.name becomes nil, I'd like ChildUI to hide.
When I do this, via unwrapping of the Model.name, then only the first value is captured by the Child who is now in control of that state. Subsequent changes will not push upstream because it is not a Binding.
Question
Can I have a non-optional upstream bind to an optional when it exists? (are these the right words?)
Complete Example
import SwiftUI
struct Child: View {
// within Child, I'd like the value to be NonOptional
#State var text: String
var body: some View {
TextField("OK: ", text: $text).multilineTextAlignment(.center)
}
}
class Model: ObservableObject {
// within the parent, value is Optional
#Published var name: String? = nil
}
struct Parent: View {
#ObservedObject var model: Model = .init()
var body: some View {
VStack(spacing: 12) {
Text("Demo..")
// whatever Child loads the first time will retain
// even on change of model.name
if let text = model.name {
Child(text: text)
}
// proof that model.name changes are in fact updating other state
Text("\(model.name ?? "<waiting>")")
}
.onAppear {
model.name = "first change of optionality works"
loop()
}
}
#State var count = 0
func loop() {
async(after: 1) {
count += 1
model.name = "updated: \(count)"
loop()
}
}
}
func async(_ queue: DispatchQueue = .main,
after: TimeInterval,
run work: #escaping () -> Void) {
queue.asyncAfter(deadline: .now() + after, execute: work)
}
struct OptionalEditingPreview: PreviewProvider {
static var previews: some View {
Parent()
}
}
Child should take a Binding to the non-optional string, rather than using #State, because you want it to share state with its parent:
struct Child: View {
// within Child, I'd like the value to be NonOptional
#Binding var text: String
var body: some View {
TextField("OK: ", text: $text).multilineTextAlignment(.center)
}
}
Binding has an initializer that converts a Binding<V?> to Binding<V>?, which you can use like this:
if let binding = Binding<String>($model.name) {
Child(text: binding)
}
If you're getting crashes from that, it's a bug in SwiftUI, but you can work around it like this:
if let text = model.name {
Child(text: Binding(
get: { model.name ?? text },
set: { model.name = $0 }
))
}
Bind your var like this. Using custom binding and make your child view var #Binding.
struct Child: View {
#Binding var text: String //<-== Here
// Other Code
if model.name != nil {
Child(text: Binding($model.name)!)
}
I previously asked a question about how to push a view with data received from an asynchronous callback. The method I ended up with has turned out to cause a Memory Leak.
I'm trying to structure my app with MVVM for SwiftUI, so a ViewModel should publish another ViewModel, that a View then knows how to present on screen. Once the presented view is dismissed from screen, I expect the corresponding ViewModel to be deinitialised. However, that's never the case with the proposed solution.
After UserView is dismissed, I end up having an instance of UserViewModel leaked in memory. UserViewModel never prints "Deinit UserViewModel", at least not until next time a view is pushed on pushUser.
struct ParentView: View {
#ObservedObject var vm: ParentViewModel
var presentationBinding: Binding<Bool> {
.init(get: { vm.pushUser != nil },
set: { isPresented in
if !isPresented {
vm.pushUser = nil
}
}
)
}
var body: some View {
VStack {
Button("Get user") {
vm.getUser()
}
Button("Read user") {
print(vm.pushUser ?? "No userVm")
}
if let userVm = vm.pushUser {
NavigationLink(
destination: UserView(vm: userVm),
isActive: presentationBinding,
label: EmptyView.init
)
}
}
}
}
class ParentViewModel: ObservableObject {
#Published var pushUser: UserViewModel? = nil
var cancellable: AnyCancellable?
private func fetchUser() -> AnyPublisher<User, Never> {
Just(User.init(id: "1", name: "wiingaard"))
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
func getUser() {
cancellable = api.getUser().sink { [weak self] user in
self?.pushUser = UserViewModel(user: user)
}
}
}
struct User: Identifiable {
let id: String
let name: String
}
class UserViewModel: ObservableObject, Identifiable {
deinit { print("Deinit UserViewModel") }
#Published var user: User
init(user: User) { self.user = user }
}
struct UserView: View {
#ObservedObject var vm: UserViewModel
var body: some View {
Text(vm.user.name)
}
}
After dismissing the UserView and I inspect the Debug Memory Graph, I see an instance of UserViewModel still allocated.
The top reference (view.content.vm) has kind: (AnyViewStorage in $7fff57ab1a78)<ModifiedContent<UserView, (RelationshipModifier in $7fff57ad2760)<String>>> and hierarchy: SwiftUI.(AnyViewStorage in $7fff57ab1a78)<SwiftUI.ModifiedContent<MyApp.UserView, SwiftUI.(RelationshipModifier in $7fff57ad2760)<Swift.String>>> AnyViewStorageBase _TtCs12_SwiftObject
What's causing this memory leak, and how can I remove it?
I can see that ViewModel is deinit() if you use #State in your View, and listen to your #Publisher in your ViewModel.
Example:
#State var showTest = false
NavigationLink(destination: SessionView(sessionViewModel: outgoingCallViewModel.sessionViewModel),
isActive: $showTest,
label: { })
.isDetailLink(false)
)
.onReceive(viewModel.$showView, perform: { show in
if show {
showTest = true
}
})
if you use viewModel.$show in your NavigationLink as isActive, viewModel never deinit().
Please refer to this post (https://stackoverflow.com/a/62511130/11529487), it solved the issue for the memory leak bug in SwiftUI by adding on the NavigationView:
.navigationViewStyle(StackNavigationViewStyle())
However it breaks the animation, there is a hacky solution to this issue. The animation problem occurs because of the optional chaining in "if let".
When setting "nil" as the destination in the NavigationLink, it essentially does not go anywhere, even if the "presentationBinding" is true.
I invite you to try this piece of code as it fixed the animtaion problem that resulted from the StackNavigationViewStyle (and no memory leaks):
Although not as pretty as the optional chaining, it does the job.
I have a model type which looks like this:
enum State {
case loading
case loaded([String])
case failed(Error)
var strings: [String]? {
switch self {
case .loaded(let strings): return strings
default: return nil
}
}
}
class MyApi: ObservableObject {
private(set) var state: State = .loading
func fetch() {
... some time later ...
self.state = .loaded(["Hello", "World"])
}
}
and I'm trying to use this to drive a SwiftUI View.
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List($api.state.strings) {
Text($0)
}
}
}
}
It's about here that my assumptions fail. I'm trying to get a list of the strings to render in my List when they are loaded, but it won't compile.
The compiler error is Generic parameter 'Subject' could not be inferred, which after a bit of googling tells me that bindings are two-way, so won't work with both my private(set) and the var on the State enum being read-only.
This doesn't seem to make any sense - there is no way that the view should be able to tell the api whether or not it's loading, that definitely should be a one-way data flow!
I guess my question is either
Is there a way to get a one-way binding in SwiftUI - i.e. some of the UI will update based on a value it cannot change.
or
How should I have architected this code! It's very likely that I'm writing code in a style which doesn't work with SwiftUI, but all the tutorials I can see online neatly ignore things like loading / error states.
You don't actually need a binding for this.
An intuitive way to decide if you need a binding or not is to ask:
Does this view need to modify the passed value ?
In your case the answer is no. The List doesn't need to modify api.state (as opposed to a textfield or a slider for example), it just needs the current value of it at any given moment. That is what #State is for but since the state is not something that belongs to the view (remember, Apple says that each state must be private to the view) you're correctly using some form of an ObservableObject (through Environment).
The final missing piece is to mark any of your properties that should trigger an update with #Published, which is a convenience to fire objectWillChange signals and instruct any observing view to recalculate its body.
So, something like this will get things done:
class MyApi: ObservableObject {
#Published private(set) var state: State = .loading
func fetch() {
self.state = .loaded(["Hello", "World"])
}
}
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List(api.state.strings ?? [], id: \.self) {
Text($0)
}
}
}
}
Not exactly the same problem as I had, but the following direction can help you possibly find a good result when bindings are done with only reads.
You can create a custom binding using a computed property.
I needed to do exactly this in order to show an alert only when one was passed into an overlay.
Code looks something along these lines :
struct AlertState {
var title: String
}
class AlertModel: ObservableObject {
// Pass a binding to an alert state that can be changed at
// any time.
#Published var alertState: AlertState? = nil
#Published var showAlert: Bool = false
init(alertState: AnyPublisher<AlertState?, Never>) {
alertState
.assign(to: &$alertState)
alertState
.map { $0 != nil }
.assign(to: &$showAlert)
}
}
struct AlertOverlay<Content: View>: View {
var content: Content
#ObservedObject var alertModel: AlertModel
init(
alertModel: AlertModel,
#ViewBuilder content: #escaping () -> Content
) {
self.alertModel = alertModel
self.content = content()
}
var body: some View {
ZStack {
content
.blur(radius: alertModel.showAlert
? UserInterfaceStandards.blurRadius
: 0)
}
.alert(isPresented: $alertModel.showAlert) {
guard let alertState = alertModel.alertState else {
return Alert(title: Text("Unexected internal error as occured."))
}
return Alert(title: Text(alertState.title))
}
}
}
I'm currently playing with Combine and SwiftUI and have built a prototype app using the MVVM pattern. The app utilises a timer and the state of the button controlling this is bound (inelegantly) to the view model utilising a PassThroughSubject.
When the button is pressed, this should toggle the value of a state variable; the value of this is passed to the view model's subject (using .send) which should send a single event per button press. However, there appears to be recursion or something equally weird going on as multiple events are sent to the subject and a runtime crash results without the UI ever being updated.
It's all a bit puzzling and I'm not sure if this is a bug in Combine or I've missed something. Any pointers would be much appreciated. Code below - I know it's messy ;-) I've trimmed it down to what appears to be relevant but let me know if you need more.
View:
struct ControlPanelView : View {
#State private var isTimerRunning = false
#ObjectBinding var viewModel: ControlPanelViewModel
var body: some View {
HStack {
Text("Case ID") // replace with binding to viewmode
Spacer()
Text("00:00:00") // repalce with binding to viewmodel
Button(action: {
self.isTimerRunning.toggle()
self.viewModel.apply(.isTimerRunning(self.isTimerRunning))
print("Button press")
}) {
isTimerRunning ? Image(systemName: "stop") : Image(systemName: "play")
}
}
// .onAppear(perform: { self.viewModel.apply(.isTimerRunning(self.isTimerRunning)) })
.font(.title)
.padding(EdgeInsets(top: 0, leading: 32, bottom: 0, trailing: 32))
}
}
Viewmodel:
final class ControlPanelViewModel: BindableObject, UnidirectionalDataType {
typealias InputType = Input
typealias OutputType = Output
private let didChangeSubject = PassthroughSubject<Void, Never>()
private var cancellables: [AnyCancellable] = []
let didChange: AnyPublisher<Void, Never>
// MARK:- Input
...
private let isTimerRunningSubject = PassthroughSubject<Bool, Never>()
....
enum Input {
...
case isTimerRunning(Bool)
...
}
func apply(_ input: Input) {
switch input {
...
case .isTimerRunning(let state): isTimerRunningSubject.send(state)
...
}
}
// MARK:- Output
struct Output {
var isTimerRunning = false
var elapsedTime = TimeInterval(0)
var concernId = ""
}
private(set) var output = Output() {
didSet { didChangeSubject.send() }
}
// MARK:- Lifecycle
init(timerService: TimerService = TimerService()) {
self.timerService = timerService
didChange = didChangeSubject.eraseToAnyPublisher()
bindInput()
bindOutput()
}
private func bindInput() {
utilities.debugSubject(subject: isTimerRunningSubject)
let timerToggleStream = isTimerRunningSubject
.subscribe(isTimerRunningSubject)
...
cancellables += [
timerToggleStream,
elapsedTimeStream,
concernIdStream
]
}
private func bindOutput() {
let timerToggleStream = isTimerRunningSubject
.assign(to: \.output.isTimerRunning, on: self)
...
cancellables += [
timerToggleStream,
elapsedTimeStream,
idStream
]
}
}
In your bindInput method isTimerRunningSubject subscribes to itself. I suspect this is not what you intended and probably explains the weird recursion you are describing. Maybe you're missing a self. somewhere?
Also odd is that both bindInput and bindOutput add all streams to the cancellables array, so they'll be in there twice.
Hope this helps.
This example works as expected but I discovered during the process that you cannot use the pattern in the original code (internal Structs to define Input and Output) with #Published. This causes some fairly odd errors (and BAD_ACCESS in a Playground) and is a bug that was reported in Combine beta 3.
final class ViewModel: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
#Published var isEnabled = false
private var cancelled = [AnyCancellable]()
init() {
bind()
}
private func bind() {
let t = $isEnabled
.map { _ in }
.eraseToAnyPublisher()
.subscribe(didChange)
cancelled += [t]
}
}
struct ContentView : View {
#ObjectBinding var viewModel = ViewModel()
var body: some View {
HStack {
viewModel.isEnabled ? Text("Button ENABLED") : Text("Button disabled")
Spacer()
Toggle(isOn: $viewModel.isEnabled, label: { Text("Enable") })
}
.padding()
}
}