I want to trigger the update of a subview in SwiftUI by changing the state of a #State variable, since it does not update alone when I change the Wallet Object since it is defined as EnvironmentObject. The thing is that I have to initialize the view with environmentObject, and it returns Some View, and cannot cast it to WalletView as it should seem that it should.
var walletView = WalletView().environmentObject(Wallet(cards: reminders))
if walletView = walletView as? WalletView{
walletView.isPresented = !walletView.isPresented
}
How can I access the WalletView object?
I've tried:
var walletView = WalletView()
let someWalletView = walletView.environmentObject(Wallet(cards: reminders))
walletView.isPresented = !walletView.isPresented
but the walletView doesn't seem to update. Any clue?
The SwiftUI approach is to change view state inside view, so as far as I understood what your going to do with WalletView it could be achieved like in the following (scratchy):
struct WalletView: View {
...
var body: some View {
_some_internal_view
.onAppear { self.isPresented = true }
.onDisappear { self.isPresented = false }
}
}
I've solved this by changing the variable isPresented to #Binding, and modifying it then triggers an update on the subclass.
Related
My app is leaking model objects because it the objects are keeping closures that are retaining the view itself.
It's better to show by an example.
In the code below, Model is not deallocated after the ContentView disappears.
//
// Content View is an owner of `Model`
// It passes it to `ViewB`
//
// When button is tapped, ContentView executes action
// assigned to Model by the ViewB
//
struct ContentView: View {
#StateObject private var model = Model()
var body: some View {
VStack {
Button(action: {
model.action?()
}) {
Text("Tap")
}
ViewB(model: model)
}
.frame(width: 100, height: 100)
.onDisappear {
print("CONTENT DISAPPEAR")
}
}
}
struct ViewB: View {
#ObservedObject var model: Model
var body: some View {
Color.red.frame(width: 20, height: 20)
.onAppear {
//
// DANGER:
// Assigning this makes a leak and Model is never deallocated.
// This is because the closure is retaining 'self'
// But since it's a struct, how can we break the cycle here?
//
model.action = { bAction() }
}
}
private func bAction() {
print("Hey!")
}
}
class Model: ObservableObject {
var action: (() -> Void)?
deinit {
print("MODEL DEINIT")
}
}
I'm not sure why there's some kind of retain cycle occurring here.
Since View is a struct, referencing it in a closure should be safe, right?
Ahoy #msmialko, while I can't give much reasoning for what I've observed, hopefully this will be a step in the right direction.
I decided to remove SwiftUI's memory management from the equation and tested with simple value and reference types:
private func doMemoryTest() {
struct ContentView {
let model: Model
func pressButton() {
model.action?()
}
}
struct ViewB {
let model: Model
func onAppear() {
model.action = action
// { [weak model] in
// model?.action = action
// }()
}
func onDisappear() {
print("on ViewB's disappear")
model.action = nil
}
private func action() {
print("Hey!")
}
}
class Model {
var action: (() -> Void)?
deinit {
print("*** DEALLOCATING MODEL")
}
}
var contentView: ContentView? = .init(model: Model())
var viewB: ViewB? = .init(model: contentView!.model)
contentView?.pressButton()
viewB?.onAppear()
contentView?.pressButton()
// viewB?.onDisappear()
print("Will remove ViewB's reference")
viewB = nil
print("Removed ViewB's reference")
contentView?.pressButton()
print("Will remove ContentView's reference")
contentView = nil
print("Removed ContentView's reference")
}
When I ran the code above, this was the console output (no deallocation of Model, as you observed):
Hey!
Will remove ViewB's reference
Removed ViewB's reference
Hey!
Will remove ContentView's reference
Removed ContentView's reference
In the above example it looks like I'm in complete control of the reference count on Model, however when I inspected the memory graph in Xcode, I could confirm that Model was retaining itself via action.context (I'm not sure what that means):
To fix the retain cycle with minimal changes, you might want to consider removing Model's action assignment using ViewB.onDisappear as I've done in my example. When I uncommented viewB?.onDisappear() then I saw the following console output:
Hey!
on ViewB's disappear
Will remove ViewB's reference
Removed ViewB's reference
Will remove ContentView's reference
*** DEALLOCATING MODEL
Removed ContentView's reference
Good luck!
Model is not a struct, it is an ObservableObject which is of type AnyObject which is an Object
you should apply weak to in the capture list for .onAppear
.onAppear { [weak model] }
I think you could also just capture model incase its self that the issue is on
.onAppear { [model] }
I have a custom View, and in this custom View I declared var isSelected: false that is gonna be toggle when taping on the view.
After I add two of those custom Views in my ViewController.
What I need is: When I select one of view, the other one is immediately deselected, so only one can be selected at the same time.
I don't have much knowledge about it, but I assume that with RxCocoa (or ideally RxSwift) it might be possible to set this isSelected variable of each view as an observable, and then in the subscription, set the other one to false once it turns true.
Help will be much appreciated, thank you in advance.
I know what you are asking for seems like a reasonable idea, but it's not. Your isSelected boolean won't change state unless you specifically write code to make it change state. That begs the question, why is other code monitoring your view's isSelected boolean rather than the event that causes the boolean to change state? Why the middle man? Especially a middle-man that is part of your View system, not your Model.
The appropriate solution is to have an Observable in your model that is bound to your two views...
Better would be something like:
class CustomView: UIView {
var isSelected: Bool = false
}
class Example {
let viewA = CustomView()
let viewB = CustomView()
let disposeBag = DisposeBag()
func example(model: Observable<Bool>) {
disposeBag.insert(
model.bind(to: viewA.rx.isSelected),
model.map { !$0 }.bind(to: viewB.rx.isSelected)
)
}
}
extension Reactive where Base: CustomView {
var isSelected: Binder<Bool> {
Binder(base, binding: { view, isSelected in
view.isSelected = isSelected
})
}
}
I'm using SwiftUI and I want to animate a view as soon as it appears (the explicit type of animation does not matter) for demo purposes in my app.
Let's say I just want to scale up my view and then scale it down to its original size again, I need to be able to animate the view to a new state and back to the original state right afterward.
Here's the sample code of what I've tried so far:
import SwiftUI
import Combine
struct ContentView: View {
#State private var shouldAnimate = false
private var scalingFactor: CGFloat = 2
var body: some View {
Text("hello world")
.scaleEffect(self.shouldAnimate ? self.scalingFactor : 1)
.onAppear {
let animation = Animation.spring().repeatCount(1, autoreverses: true)
withAnimation(animation) {
self.shouldAnimate.toggle()
}
}
}
Obviously this does not quite fulfill my requirements, because let animation = Animation.spring().repeatCount(1, autoreverses: true) only makes sure the animation (to the new state) is being repeated by using a smooth autoreverse = true setting, which still leads to a final state with the view being scaled to scalingFactor.
So neither can I find any property on the animation which lets my reverse my animation back to the original state automatically (without me having to interact with the view after the first animation), nor did I find anything on how to determine when the first animation has actually finished, in order to be able to trigger a new animation.
I find it pretty common practice to animate some View upon its appearance, e.g. just to showcase that this view can be interacted with, but ultimately not alter the state of the view. For example animate a bounce effect on a button, which in the end sets the button back to its original state. Of course I found several solutions suggesting to interact with the button to trigger a reverse animation back to its original state, but that's not what I'm looking for.
Here is a solution based on ReversingScale animatable modifier, from this my answer
Update: Xcode 13.4 / iOS 15.5
Complete test module is here
Tested with Xcode 11.4 / iOS 13.4
struct DemoReverseAnimation: View {
#State var scalingFactor: CGFloat = 1
var body: some View {
Text("hello world")
.modifier(ReversingScale(to: scalingFactor, onEnded: {
DispatchQueue.main.async {
self.scalingFactor = 1
}
}))
.animation(.default)
.onAppear {
self.scalingFactor = 2
}
}
}
Another approach which works if you define how long the animation should take:
struct ContentView: View {
#State private var shouldAnimate = false
private var scalingFactor: CGFloat = 2
var body: some View {
Text("hello world")
.scaleEffect(self.shouldAnimate ? self.scalingFactor : 1)
.onAppear {
let animation = Animation.easeInOut(duration: 2).repeatCount(1, autoreverses: true)
withAnimation(animation) {
self.shouldAnimate.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation(animation) {
self.shouldAnimate.toggle()
}
}
}
}
}
In this example, when I drag across the screen why does LabelViewRepresentable get re-initialized before every "updateUIView" call? If I make the counter a #State property instead of an #EnvironmentObject property, it only initializes once like I would expect.
import SwiftUI
class Counter: ObservableObject {
#Published var count = 0
}
struct CounterView: View {
#EnvironmentObject var counter: Counter
var body: some View {
LabelViewRepresentable(count: $counter.count)
.gesture(DragGesture().onChanged({ _ in
self.counter.count += 1
}))
}
}
struct LabelViewRepresentable: UIViewRepresentable {
#Binding var count: Int
private var view: UILabel
init(count: Binding<Int>) {
print("init")
let label = UILabel()
label.text = "0"
self.view = label
self._count = count
}
func makeUIView(context: UIViewRepresentableContext<LabelViewRepresentable>) -> UILabel {
print("makeUIView")
return view
}
func updateUIView(_ uiView: UILabel, context: UIViewRepresentableContext<LabelViewRepresentable>) {
print("updateUIView")
view.text = "\(count)"
}
}
When you look at Apple docs about EnvironmentObject you will find this:
A dynamic view property that uses a bindable object supplied by an
ancestor view to invalidate the current view whenever the bindable
object changes.
That means that each time an EnvironmentObject changes all views that are dependent on it get reinitialised and redrawn.
It works slightly different with State, in Apple docs it is described as follows:
A persistent value of a given type, through which a view reads and
monitors the value.
The view cannot get reinitialised when the State changes as the State value would get discarded. The parts that are influenced by State will get redrawn. On the other hand any children of the view that have the State value passed in as a binding will get reinitialised.
lazy var headerView: WatchlistModifierHeaderView = {
let view = WatchlistModifierHeaderView()
view.translatesAutoresizingMaskIntoConstraints = false
view.heightAnchor.constraint(equalToConstant: HEADER_VIEW_HEIGHT).isActive = true
view.tapEventer.handler = { [unowned self] in
print("HeaderView tapped")
}
return view
}()
Here is an example. I like this style because everything pertaining to the view is captured in the lazy var. However, I am curious if the variable won't deinit because of the callback.
At the end of the day, I need to read up on memory, init, and deinit.
That looks fine to me. If you actually end up using self in the callback as long as you keep that unowned or add a weak, it shouldn't cause a retain cycle and will deinit correctly.