How update a SwiftUI List without animation - ios

I want to update a SwiftUI List without any insert animation.
My List is getting its data from an #EnvironmentObject
I already tried to wrap the List itself and the PassthroughSubject.send() in a withAnimation(.empty) block but this does not help.
A very very dirty workaround is to call UIView.setAnimationsEnabled(false) (yes, UIKit has impact on SwiftUI), but there must be a SwiftUI-like way to set custom insert animations.

While the answer provided by DogCoffee works, it does so in an inefficient manner. Sometimes we do have to force the system to do what we want by being inefficient. In the case of implicit animations in SwiftUI, there is a better way to disable them.
Using the Transaction mechanism in SwiftUI, we can define an extension that can be applied to any view. This will disable animations for the view and any children.
For the list view example, this avoids replacing all the data in the list with a new, but identical copies.
extension View {
func animationsDisabled() -> some View {
return self.transaction { (tx: inout Transaction) in
tx.disablesAnimations = true
tx.animation = nil
}.animation(nil)
}
}
Try applying this extension to your list, or the parent containing view. You may have to experiment to find which view is ideal.
List {
// for each etc
}.animationsDisabled()

This works, just place .id(UUID()) at the end of your list
List {
// for each etc
}.id(UUID())
Sort of like reloadData for UIKit

on tvOS this works for me:
List {
...
}
.animation(.none)

.animate(nil)
you can find more info on https://developer.apple.com/tutorials/swiftui/animating-views-and-transitions

Related

How to provide a ‘System’ appearance choice in SwiftUI?

Main Question
Sorry for the long title. I’m trying to figure out how to provide a choice to user’s for setting the ColorScheme of the app to ‘System’.
As in, have the app default to the system’s dark mode / light mode. I know UIKit has a way to set the style to .unspecified but I’m not sure how to access that in SwiftUI.
I already tried setting .preferredColor() to nil and it kind of sort of works but sometimes it doesn’t. I’m new to managing state in Swift so I’m definitely doing something wrong.
Side Question:
When I set .preferredColor() to say .dark - the modal settings sheet I have doesn’t update? It always stays the system colour. I have .preferredColor() triggering on the ContentView() in the ...App.swift file.
Update
I solved my Side Question by just adding .preferredColor() to the modal sheet as well. It's not ideal but it does work. However I'm still unable to figure out how to set a .system preference in SwiftUI
I ended up finding two solutions to this problem. The first was found by jinjie and the other by me. Both solutions can be viewed here - repo. created by rizwankce.
Solution
What I ended up doing was using the package Introspect to grab the UIViewController off the main view I have using a ViewModifier.
Within the ViewModifier I just used .introspectViewController and that gave me the UIViewController so that I could then set UIViewController.overrideUserInterfaceStyle = .unspecified like this. Then I just did this conditionally based on a Picker not seen below. ⤵
/// All themes
enum Themes: String {
case Dark
case Light
case System
}
// MARK: - Theme Switch
struct ThemeSwitch: ViewModifier {
let appStorage: String
func body(content: Content) -> some View {
content
.introspectViewController { UIViewController in
switch appStorage {
case Themes.System.rawValue: UIViewController.overrideUserInterfaceStyle = .unspecified
case Themes.Dark.rawValue: UIViewController.overrideUserInterfaceStyle = .dark
case Themes.Light.rawValue: UIViewController.overrideUserInterfaceStyle = .light
default: UIViewController.overrideUserInterfaceStyle = .unspecified
}
}
}
}
// MARK: - Extensions
extension View {
func themeSwitch(appStorage: String) -> some View {
modifier(ThemeSwitch(appStorage: appStorage))
}
}
I just stored the desired theme as a String into a #State object which also updated an #AppStorage object so the app always had the selection no matter what.
You don't have to use Introspect and could instead create your own wrapper but I already had this package installed so I figured why not.
Alternative Solutions
The other solution is to make a view modifier and display the modified view conditionally. The condition is based on an optional array, dictionary, etc. of type ColorScheme. It either unwraps the optional if it's not nil or it just shows the unmodified view. You can find it at the repo. mentioned above under closed issues or pull requests.
In the future hopefully Apple will provide a way to do this natively but for now this will do! I'll try to update this answer in the future if something better comes along.

SwiftUI: Change only one instance of UITableViewAppearance

I know that the component List has an UITableView behind. I also know I can change the background by changing the UITableView.appearance().backgroundColor.
What I want is to change this for a single View that has a List component, without affecting the rest of the application. Is this possible? If so, how?
Thanks!
This is what I know needs to be done to achieve this:
var body: some View {
List {
//Your content
}
.onAppear {
//The specific table view settings.
//UITableView.appearance().separatorColor = .black
}
.onDisappear {
//The general table view settings
//UITableView.appearance().separatorColor = nil
}
}
But it is problematic for several reasons.
The biggest reason is the fact you might have 2 table views in the same View and you want them to have different appearances.
The second issue is that not all cases will call onAppear (I don't remember specific cases but I remember having issues with TabViews)
I hope there's already a better solution waiting in the next version of SwiftUI.

How can I detect a subview being added anywhere in the view hierarchy (including subviews)?

I am creating a framework for the other apps to use it. I want to find when the display presented to the user changes. These changes include addition and removal of subviews, scrolling down, adding text, etc. Is there a way I can directly check when the content presented on the screen is changing. Above question is a part of the problem.
Did you mean viewDidLoad?
That function called first time after all view loaded same as ViewTreeObserver.OnGlobalLayoutListener.
After you explanations I would simply do something like this:
class MyViewController:UIScrollViewDelegate{
func addSubview(){
self.takeSnaphot()
}
func scrollViewDidScroll(scrollView:UIScrollView){
self.takeSnaphot()
}
func takeSnaphot(){
//the code to take snaphots
}
}

show a view on 2 viewcontrollers

I'm looking for a way to show a UIView "InventoryView" in 2 view controllers.
I'm working on an inventory system for my game that I trying to make but I need to be able to access it from my main view, where it will go to a InventoryViewController (in this ViewController is my InventoryView) but I also need to be able to access the InventoryView from my BattleViewController where it does not go to my InventoryViewController but where it print the InventoryView on my BattleViewController so I can access everything durning the battle.
Example:
(evrything is dragand drop, the UIView and the UIButtons)
InventoryViewController
class InventoryViewController: UIViewController {
class InventoryView: UIView {
//here are some UIButtons and labels
}
}
BattleViewController
class BattleViewController: UIViewController {
class InventoryView: UIView {
//it should print the Inventory Screen on my BattleViewController
//here are the same properties as it shows in the InventoryViewController
}
}
This is a great example to look at the way OOP programming works best.
Ask yourself the following questions:
What is the purpose of the view?
Are the interactions on the view homogenous across all the instances? (touch events, specific behavior, etc...)
What is the minimum amount of information you need to make the view look the way you want?
Once you have those answers, you can approach the concept of reusability of views safely.
The way to go about it is to subclass UIView, create the necessary elements of your view, setup your constraints (still in the view, either in a nib or programmatically), and implement any behavior that will be consistent across views (For example if the view is a segmented control, every time you click a segment all the others go grey and the one you clicked go blue. Since that's the primary purpose of the segmented control, the code for it should belong to the segmented control).
Chances are you will find the docs very useful: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/
Lastly write a setup method that takes all the information you need and sets up all your graphical elements accordingly. Remember, views should never own their data (they should be templates, the controller will provide the data).
I have no idea what you view looks like but I assume the inventory is represented as an object. Then something like could be a good start:
class InventoryView: UIView {
var inventory: Inventory? {
didSet {
if let newInventory = inventory { //in case we set it to nil
setup(withInventory: newInventory)
}
}
}
private func setup(withInventory inventory: Inventory) {
//do your setup here
}
}
Then in your controller you can call:
let inventoryView = InventoryView()
inventoryView.inventory = myPlayer.inventory
You cannot use a view in two places, at least not within the UI. Every view can be added to only one super view at a time.
If you need the same contents to be displayed twice, create a UIViewController class which's view contains the common UI, create two of those and add them to your UI.

Using property observers to modify UI components in Swift

I have created a subclass of a UICollectionViewCell that shows some information. I have one property in with type Weather. When an instance of that is set I want to update the cell. Is the approach below bad? I am thinking of that I may trigger the view to be created to early if I access the UI components before it is loaded. Or is that non sense and only applies to UIViewController (with regard to using view property to early)?
If this is bad, what would be the correct way?
var weather: Weather? {
didSet {
if let weather = weather {
dayLabel.text = dayFormatter.stringFromDate(weather.fromDate)
// ... more code like this
}
}
}
You may want an else clause, though, clearing the text field if weather was nil. Likewise, if you might update this from a background thread, you might want to dispatch that UI update back to the main thread.
Be aware that this observer is not called when you set weather in the cell's init (nor would be the #IBOutlet be configure at that point, anyway). So make sure that you're not relying upon that.
Also, if Weather is mutable, recognize that if you change the fromDate of the existing Weather object, this won't capture that. (If Weather was mutable, you'd really want to capture its changing properties via KVO, a delegate-protocol pattern, or what have you.) But if you make Weather immutable, you should be fine.
So, technically, that's the answer to the question, but this raises a few design considerations:
One generally should strive to have different types loosely coupled, namely that one type should not be too reliant on the internal behavior of another. But here we have an observer within the cell which is dependent upon the mutability of Weather.
This use of a stored property to store a model object within view is inadvisable. Cells are reused as they scroll offscreen, but you probably want a separate model that captures the relevant model objects, the controller then handles the providing of the appropriate model object to the view object (the cell) as needed.
Bottom line, it's not advisable to use a stored property for "model" information inside a "view".
You can tackle both of these considerations by writing code which makes it clear that you're only using this weather parameter solely for the purpose of updating UI controls, but not for the purposes of storing anything. So rather that a stored property, I would just use a method:
func updateWithWeather(weather: Weather?) {
if let weather = weather {
dayLabel.text = dayFormatter.stringFromDate(weather.fromDate)
// ... more code like this
} else {
dayLabel.text = nil
// ... more code like this
}
}
And this would probably only be called from within collectionView:cellForItemAtIndexPath:.
But, this makes it clear that you're just updating controls based upon the weather parameter, but not trying to do anything beyond that. And, coincidentally, the mutability of the weather object is now irrelevant, as it should be. And if the model changes, call reloadItemsAtIndexPaths:, which will trigger your collectionView:cellForItemAtIndexPath: to be called.
There are times where a stored property with didSet observer is a useful pattern. But this should be done only when the property is truly a property of view. For example, consider a custom view that draws some shape. You might have stored properties that specify, for example, the width and the color of the stroke to be used when drawing the path. Then, having stored properties for lineWidth and strokeColor might make sense, and then you might have a didSet that calls setNeedsDisplay() (which triggers the redrawing of the view).
So, the pattern you suggest does have practical applications, it's just that it should be limited to those situations where the property is truly a property of the view object.
I would use a property observer if I planned up updating the value during the users session. If this is a value that only gets updated when the user first loads, I would just simply call a method when my view is initially loaded.
If you use a property observer, you can give it an initial value when you define it so the data is there when the user needs it. Also, if you're updating the user interface, make sure you do it on the main queue.
var weather: Weather = data {
didSet {
dispatch_async(dispatch_get_main_queue(),{
if let weather = weather {
dayLabel.text = dayFormatter.stringFromDate(weather.fromDate)
// ... more code like this
}
})
}
}

Resources