I want to show a view while tasks complete, how else could I call a SwiftView to show without using conditionals based on a State bool? Basically I want to avoid having this hardcoded below for every file that is gonna need the loading view...
struct ContentView: View {
#State var doIWantThisViewToShow: Bool = false
var body: some View {
VStack {
Button("Show/Hide MyView") {
doIWantThisViewToShow.toggle()
}
if doIWantThisViewToShow {
MyView()
.padding()
}
}
}
}
You can explore a few strategies to reduce duplicate code.
Custom Environment value to avoid passing #Binding
Reusable effects
Maybe a protocol if your state is complex (probably not)
Mutating a view hierarchy
A custom EnvironmentValue broadcasts a state down to child views. This will save you from passing #Binding through views that may not consume the value.
Keep in mind this is a one-way top-down broadcast. Unlike #Binding, children can't mutate parent state. (But they can mutate their own children's knowledge of said state.)
Set in Parent
#State var isHovered = false
var parent: some View {
///
.environment(\.parentIsHovered, isHovered)
}
Observe in Child
#Environment(\.parentIsHovered) var parentIsHovered
var child: some View {
///
.grayscale(parentIsHovered ? 0 : 0.9)
.animation(.easeInOut, value: parentIsHovered)
}
Define
private struct ParentIsHoveredKey: EnvironmentKey {
static let defaultValue: Bool = false
}
extension EnvironmentValues {
var parentIsHovered: Bool {
get { return self[ParentIsHoveredKey] }
set { self[ParentIsHoveredKey] = newValue }
}
}
Reusable effects
If you gray out certain views or display a loading indicator, you can use a ViewModifier that accepts a Binding and conditionally displays an overlay, background or effects.
The example below demonstrates that by linking the .animation API to accessibilityReduceMotion.
// view
.animate(.easeOut(duration: .fast), value: isLoading)
extension View {
func animate<E: Equatable>(_ animation: Animation?, value: E) -> some View {
self.modifier(AccessibleAnimationModifier(animation, for: value))
}
}
struct AccessibleAnimationModifier<E: Equatable>: ViewModifier {
#Environment(\.accessibilityReduceMotion) var reduceMotion
init(_ animation: Animation? = .default, for value: E) {
self.animation = animation
self.value = value
}
var animation: Animation?
var value: E
func body(content: Content) -> some View {
content
.animation(reduceMotion ? .none : animation, value: value)
}
}
Reacting to loading state
Unless you handle loading state through some observed class, you need to store that state in your View using #State.
Maybe a protocol with a default implementation in an extension helps reduce duplicate code in computing a complex loading state between Views.
The pseudocode below defines a protocol DragSource with functions returning an NSItemProvider. The extension provides default implementations that a View or VM can call.
protocol DragSource {
func makeDraggableThing1(/// content + logic objects) -> NSItemProvider
}
extension DragSource {
func makeDraggableThing1(///) -> NSItemProvider {
/// Default work I only want to edit once in the app
}
}
Related
This question already has answers here:
What is the difference between #StateObject and #ObservedObject in child views in swiftUI
(3 answers)
Closed 3 months ago.
Here's a hypothetical master/detail pair of SwiftUI views that presents a button which uses NavigationLink:value:label: to navigate to a child view. The child view uses MVVM and has a .navigationTitle modifier that displays a placeholder until the real value is set (by a network operation that is omitted for the sake of brevity).
Upon first launch, tapping the button does navigate to the child view, but the "Loading child..." navigationTitle placeholder never changes to the actual value of "Alice" despite being set in the viewmodel's loadChild() method. If you navigate back and tap the button again, all subsequent navigations do set the navigationTitle correctly.
However, the child view has an if condition. If that if condition is replaced with Text("whatever") and the app is re-built and re-launched, the navigationTitle gets set properly every time. Why does the presence of an if condition inside the view affect the setting of the view's navigationTitle, and only on the first use of navigation?
import SwiftUI
// MARK: Data Structures
struct AppDestinationChild: Identifiable, Hashable {
var id: Int
}
struct Child: Identifiable, Hashable {
var id: Int
var name: String
}
// MARK: -
struct ChildView: View {
#ObservedObject var vm: ChildViewModel
init(id: Int) {
vm = ChildViewModel(id: id)
}
var body: some View {
VStack(alignment: .center) {
// Replacing this `if` condition with just some Text()
// view makes the navigationTitle *always* set properly,
// including during first use.
if vm.pets.count <= 0 {
Text("No pets")
} else {
Text("List of pets would go here")
}
}
.navigationTitle(vm.child?.name ?? "Loading child...")
.task {
vm.loadChild()
}
}
}
// MARK: -
extension ChildView {
#MainActor class ChildViewModel: ObservableObject {
#Published var id: Int
#Published var child: Child?
#Published var pets = [String]()
init(id: Int) {
self.id = id
}
func loadChild() {
// Some network operation would happen here to fetch child details by id
self.child = Child(id: id, name: "Alice")
}
}
}
// MARK: -
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink(value: AppDestinationChild(id: 42), label: {
Text("Go to child view")
})
.navigationDestination(for: AppDestinationChild.self) { destination in
ChildView(id: destination.id)
}
}
}
}
The point of .task is to get rid of the need for a reference type for async code, I recommend you replace your state object with state, e.g.
#State var child: Child?
.task {
child = await Child.load()
}
You could also catch an exception and have another state for an error message.
I’m looking for the proper pattern and syntax to address my goal of having an ObservableObject instance that I can share amongst multiple views, but while keeping logic associated with it contained to another class. I’m looking to do this to allow different ‘controller’ classes to manipulate the properties of the state without the view needing to know which controller is acting on it (injected).
Here is a simplification that illustrates the issue:
import SwiftUI
class State: ObservableObject {
#Published var text = "foo"
}
class Controller {
var state : State
init(_ state: State) {
self.state = state
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var state = State()
var controller: Controller!
init() {
controller = Controller(state)
}
var body: some View {
VStack {
Text(controller.state.text) // always shows 'foo'
Button("Press Me") {
print(controller.state.text) // prints 'foo'
controller.changeState()
print(controller.state.text) // prints 'bar'
}
}
}
}
I know that I can use my ObservableObject directly and manipulate its properties such that the UI is updated in response, but in my case, doing so prevents me from having different ‘controller’ instances depending on what needs to happen. Please advise with the best way to accomplish this type of scenario in SwiftUI
To make SwiftUI view follow state updates, your controller needs to be ObservableObject.
SwiftUI view will update when objectWillChange is triggered - it's done automatically for properties annotated with Published, but you can trigger it manually too.
Using same publisher of your state, you can sync two observable objects, for example like this:
class Controller: ObservableObject {
let state: State
private var cancellable: AnyCancellable?
init(_ state: State) {
self.state = state
cancellable = state.objectWillChange.sink {
self.objectWillChange.send()
}
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var controller = Controller(State())
I use the same fetch request in multiple top level views under ContentView. I mean the same entity / predicate etc.
Previously I tried doing the request once in ContentView and passing it as an array through several layers of views. However, this stopped changes, deletes etc, being propagated across other views.
I just wondered if there is another approach which would work ?
I'm thinking that some kind of singleton approach might work, but I'm worried about the performance implications, however this might outweigh having to run the request several times.
Also, I wondered about passing a request results rather than array?
However having to pass this around seems ugly.
You can use the Environment to pass your models to children without having to passing an array through several layers of views. You start by creating your own EnvirnomentKey
public struct ModelEnvironmentKey: EnvironmentKey {
public static var defaultValue: [Model] = []
}
public extension EnvironmentValues {
var models: [Model] {
get { self[ModelEnvironmentKey] }
set { self[ModelEnvironmentKey] = newValue }
}
}
public extension View {
func setModels(_ models: [Model]) -> some View {
environment(\.models, models)
}
}
I like using ViewModifiers to setup my environment, following the Single-Responsibility Principle:
struct ModelsLoaderViewModifier: ViewModifier {
#FetchRequest(entity: Model(), sortDescriptors: [])
var models: FetchedResults<Model>
func body(content: Content) -> some View {
content
.setModels(Array(models))
}
}
extension View {
func loadModels() -> some View {
modifier(ModelsLoaderViewModifier)
}
}
I then would add this modifier pretty high on the view hierarchy.
#main
struct BudgetApp: App {
#ObservedObject var persistenceManager = PersistenceManager(usage: .main)
let startup = Startup()
var body: some Scene {
WindowGroup {
ContentView()
.loadModels()
}
}
}
Now ContentView can read from the environment:
struct ContentView: View {
#Environment(\.models) var models
var body: some View {
List {
ForEach(models) { model in
Text(model.name)
}
}
}
}
I have an Observed Object that's working properly. When I update a String, the label updates.
However, I have a Bool that needs to call a custom function when it changes inside of an Observable Object. When the Bool is set to true, I need to flash the background color for 0.1 seconds.
class Event: ObservableObject {
static let current = Event()
#Published var name = ""
#Published var pass = false
}
struct EnterDistanceView: View {
#ObservedObject var event = Event.current
//when event.pass == true, call this
func flash() {
//UI update of flash
}
}
How do I call a method when a property inside of the #ObservedObject changes? Is this possible, or do I need to create
You can use onReceive to do imperative actions when a published value changes:
struct EnterDistanceView: View {
#ObservedObject var event = Event.current
func flash() {
//UI update of flash
}
var body: some View {
Text("Hello world")
.onReceive(event.$pass) { value in
if value {
//do your imperitive code here
flash()
}
}
}
}
Inside your flash method, I assume you'll want to change the value of a #State variable representing the screen color and then use DispatchQueue.main.asyncAfter to change it back shortly thereafter.
Consider we have a RootView and a DetailView. DetailView has it's own BindableObject, let's call it DetailViewModel and we have scenario:
RootView may be updated by some kind of global event e.g. missed
internet connection or by it's own data/view model
When RootView handling event it's
content is updated and this is causes new struct of DetailView to
be created
If DetailViewModel is created by DetailView on init,
there would be another reference of DetailViewModel and it's state (e.g. selected object) will be missed
How can we avoid this situation?
Store all ViewModels as EnvironmentObjects that is basically a singleton pool. This approach is causes to store unneeded objects in memory when they are not used
Pass throw all ViewModels from RootView to it's children and to children of child (has cons as above + painfull dependencies)
Store View independent DataObjects (aka workers) as EnvironmentObjects. In that case where do we store view dependent states that corresponds to Model? If we store it in View it will end up in situation where we cross-changing #States what is forbidden by SwiftUI
Better approach?
Sorry me for not providing any code. This question is on architecture concept of Swift UI where we trying to combine declarative structs and reference objects with data.
For now I don't see da way to keep references that corresponds to appropriate view only and don't keep them in memory/environment forever in their current states.
Update:
Lets add some code to see whats happening if VM is created by it's View
import SwiftUI
import Combine
let trigger = Timer.publish(every: 2.0, on: .main, in: .default)
struct ContentView: View {
#State var state: Date = Date()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: ContentDetailView(), label: {
Text("Navigation push")
.padding()
.background(Color.orange)
})
Text("\(state)")
.padding()
.background(Color.green)
ContentDetailView()
}
}
.onAppear {
_ = trigger.connect()
}
.onReceive(trigger) { (date) in
self.state = date
}
}
}
struct ContentDetailView: View {
#ObservedObject var viewModel = ContentDetailViewModel()
#State var once = false
var body: some View {
let vmdesc = "View model uuid:\n\(viewModel.uuid)"
print("State of once: \(once)")
print(vmdesc)
return Text(vmdesc)
.multilineTextAlignment(.center)
.padding()
.background(Color.blue)
.onAppear {
self.once = true
}
}
}
class ContentDetailViewModel: ObservableObject, Identifiable {
let uuid = UUID()
}
Update 2:
It seems that if we store ObservableObject as #State in view (not as ObservedObject) View keeps reference on VM
#State var viewModel = ContentDetailViewModel()
Any negative effects? Can we use it like this?
Update 3:
It seems that if ViewModel kept in View's #State:
and ViewModel is retained by closure with strong reference - deinit will never be executed -> memory leak
and ViewModel is retained by closure with weak reference - deinit invokes every time on view update, all subs will be reseted, but properties will be the same
Mehhh...
Update 4:
This approach also allows you to keep strong references in bindings closures
import Foundation
import Combine
import SwiftUI
/**
static func instanceInView() -> UIViewController {
let vm = ContentViewModel()
let vc = UIHostingController(rootView: ContentView(viewModel: vm))
vm.bind(uiViewController: vc)
return vc
}
*/
public protocol ViewModelProtocol: class {
static func instanceInView() -> UIViewController
var bindings: Set<AnyCancellable> { get set }
func onAppear()
func onDisappear()
}
extension ViewModelProtocol {
func bind(uiViewController: UIViewController) {
uiViewController.publisher(for: \.parent)
.sink(receiveValue: { [weak self] (parent) in
if parent == nil {
self?.bindings.cancel()
}
})
.store(in: &bindings)
}
}
struct ModelView<ViewModel: ViewModelProtocol>: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<ModelView>) -> UIViewController {
return ViewModel.instanceInView()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ModelView>) {
//
}
}
struct RootView: View {
var body: some View {
ModelView<ParkingViewModel>()
.edgesIgnoringSafeArea(.vertical)
}
}
Apple says that we should use ObservableObject for the data that lives outside of SwiftUI. It means you have to manage your data source yourself.
It looks like a single state container fits best for SwiftUI architecture.
typealias Reducer<State, Action> = (inout State, Action) -> Void
final class Store<State, Action>: ObservableObject {
#Published private(set) var state: State
private let reducer: Reducer<State, Action>
init(initialState: State, reducer: #escaping Reducer<State, Action>) {
self.state = initialState
self.reducer = reducer
}
func send(_ action: Action) {
reducer(&state, action)
}
}
You can pass the instance of the store into the environment of your SwiftUI app and it will be available in all views and will store your app state without data losses.
I wrote a blog post about this approach, take a look at it for more information
https://swiftwithmajid.com/2019/09/18/redux-like-state-container-in-swiftui/