Get Parent Size SwiftUI - swiftui

Is there any way of finding the parent view size using SwiftUI, I have had a look through the documentation and examples and it seems most (if not all) are hard coding sizes, ideally I want to find the size of the parent and then set the sub view size based on a percentage of the parents size (probably in some swift helper class or something) e.g.
func getSizeFromParent(fractionHeight: Int, fractionWidth: Int) -> Size
{
var parentSize = // is there a way to get parent size somehow
var newHeight = parentSize.height * fractionHeight
var newWidth = parentSize.width * fractionWidth
return Size(newHeight, newWidth)
}
Note the above code is not meant to be a working example just pseudo code

In SwiftUI parents suggest a size to their children, but children decide what to do with it. They can ignore it, use it partially, or be completely obedient.
As Radagast commented in your question, GeometryReader is the way SwiftUI uses to communicate sizes down the view tree hierarchy.
I've written a detailed article about how you can use GeometryReader and GeometryProxy and I included some examples of use cases. GeometryReader is also very useful in combination with the .background() and .overlay() modifiers, as it let you "steal" the geometry of another view.
For more information, check: https://swiftui-lab.com/geometryreader-to-the-rescue/

Related

Is SwiftUI analogous to React when it comes to rendering? does it renders only the pieces that needs change?

The information I got about react is that even if you have a large component with lots of subcomponents when the value state of one of those components change react is smart enough to update only the part of the component that needs change instead of the whole component thus being really performant during UI re-render,
My question is does SwiftUI works the same way?
If I have a Text() that is updated by #Published property inside an Observed class the value happens to be the same as before will the UI actually re-render?
what if
class StringFetcher: ObservableObject {
#Published var stringA: String = "foo"
#Published var stringB: String = "bar"
#Publisher var showScreenA: Bool = true
}
struct MyView: View {
#ObservedObject var fetcher: StringFetcher
var body: some View {
VStack {
if fetcher.showScreenA {
Text(fetcher.stringA)
} else {
Text(fetcher.stringB)
}
}
}
}
Will a change in stringB publisher trigger an UI re-rendering even if B isn't visible? at the moment?
I couldn't find much resource on how the process works, does anyone know that or know where I could read more in depth about it?
Thank you in advance )
Yes if you supply the same data to a View init as last time (e.g. the same string to Text), SwiftUI will not run the body of that View and thus that part of the View hierarchy diff will not have changed and thus it won't update those UIKit views on screen. However, if you supply the same number to Text and the region changes, then it will run its own body because it also is listening for region changes so it can update UILabel with new number formatting.
Some property wrappers cause body to run every time though, so sometimes you need to do some defensive sub View wrapping to prevent body being called unnecessarily, e.g. #FetchRequest has this problem.
Swift works most efficiently with value types like structs so always try to use those instead of objects. With property wrappers and DynamicProperty you can usually do everything in the View struct, especially now we have async/await and the task modifier.

SwiftUI: implement fae-in/out animation when inserting/removing items to list

I wonder if it's possible to customize List animation when inserting/removing/etc. items? The default animation is based on the diff result. What I'd like to have, however, is fade-in/out, just like the default transition animation between two lists.
Background: in my app I have multiple lists and user can select any one to show. Usually this should be implemented by using switch statement or if/else statement or id() modifier and SwiftUI's default transition animation should just work fine. However, due to this bug, I have to implement them by using a single list and swap in/out list content based on the current logical list user selects. Since they are different lists logically, I don't want the default diff based animation, instead I'd like to have fade-in/out animation.
I have tried to emulate the fade-in/out animation as below. While the code works, the UI effect is not satisfying. The root cause is that, to implement fade-in/out animation, I need to get the snapshot of the old list (that is, the fade-out animation should be implemented before the list was changed), which is impossible in hack like this.
MyList(listContent: selectedList)
.opacity(listOpacity)
// This emulates fade-out.
.onChange(of: selectedList) { newValue in
withAnimation {
listOpacity = 0
}
}
// This emulates fade-in.
.onChange(of: listOpacity) { newValue in
if newValue == 0 {
withAnimation {
listOpacity = 1
}
}
}
As far as I can tell, there isn't an API to customize list animation. But just in case, does anyone have suggestions on how to implement it? Thanks.

Downsides to NOT calling UIViewController.addChild, .didMove, etc when embedding views from other view controllers

What are the downsides to not following this process?
let parent = UIViewController()
let child = UIViewController()
parent.view.addSubview(child.view)
parent.addChild(child)
child.didMove(toParent: parent)
// and to remove
child.willMove(toParent: nil)
child.removeFromParent()
child.view.removeFromSuperview()
and instead just doing something more on the order of
let parent = UIViewController()
let child = UIViewController()
parent.view.addSubview(child.view)
// and to remove
child.view.removeFromSuperview()
My specific desire is to use SwiftUI views in place of UIViews sprinkled through my project, but officially you're supposed to use a UIHostingController and embed it as a child view controller of whatever parent view controller it belongs to.
I was previously under the impression that you have to call these methods, but then another developer suggested I just try not calling them with the assumption I'm only missing out on view controller lifecycle events (which I don't think matter to me in most cases). I've since tried it and it worked, but I'm worried about what I'm missing/why this might be a bad idea.
I recently came across an example of something you might lose if you don't add the UIHostingViewContoller as a child of the parent view controller in this article about using SwiftUI views in self-sizing table view cells. If you don't add it as a child, the height of the cell holding its view is not always calculated correctly.
https://noahgilmore.com/blog/swiftui-self-sizing-cells/#view-controller-containment

SwiftUI and EmptyViews

I have the following code example:
struct ContentView: View {
#State var showText = true
var body: some View {
VStack {
if self.showText {
Text("Hello")
}
//else {
// EmptyView()
// }
}
}
}
When it runs I get the text showing as normal. However, if I set showText to false it still compiles and runs as normal even though the VStack contains nothing - it just doesn't show anything when run. If I uncomment the else clause the example also runs as expected. If I remove all the contents of the VStack however, the example won't compile as nothing is returned.
So my questions are:
Is SwiftUI silently adding an EmptyView when nothing is in the VStack when showText is false ?
Should I really be adding the else clause and return an EmptyView ?
The question isn't completely academic either as I have a use case where I would prefer the whole view to be more or less 'thrown away' rather than have EmptyViews in the hierarchy, though my understanding of SwiftUI is pretty limited at the moment so it may not matter.
VStack is a function builder so it expects to get some value back from the closure. In the case of the if it invokes the buildEither version of the function builder so that satisfies the condition that its not empty. See the function builder proposal. Any ways you should not worry about the EmptyViews. A SwiftUI.View is just a blue print to build the actual view (as apple calls it a cheap value on the stack). It is not the real view object in memory and on the graphics card, as with a UIView or CALayer. The rendering system is going to translate your EmptyView into a noop. SwiftUI.Views get created and discarded all the time and are designed to be cheap unlike UIViews, and the system diff's the SwiftUI.View tree and only applies the delta to the real views in graphics memory similarl to how Flutter, React, and Android Compose work.

TabView switching between tabs animated for SwiftUI

I have a TabView and I want to do animated switching between pages, like this:
https://stackoverflow.com/a/54774397/5376525
Is it possible for SwiftUI?
I actually just implemented this using SwiftUI. Here's what I did:
1) Create a SwiftUI view that conforms to UIViewControllerRepresentable. I used the init method to provide it an array of SwiftUI views of type AnyView. There is some work to do in the makeUIViewController and the updateUIViewController methods so we'll come back to that.
I created a typealias to pass a tuple of Views, their Image Name (assuming you're using system images) and the View name. It looks like this:
typealias TabBarItemView = (ViewName: String, ImageName: String, TargetView: AnyView)
2) You'll need to create a class that conforms to the UITabBarControllerDelegate. Within that delegate, you can override the tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning method.
Part of this requires you to create another class, one that conforms to UIViewControllerAnimatedTransitioning. This class requires you to implement two functions: transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval which just needs you to specify how long you want the animation to last.
The second function is animateTransition(using transitionContext: UIViewControllerContextTransitioning) which requires you to do some heavier lifting. Within this function, you'll need to design the animation that moves the views. Essentially, you should be applying a CGAffineTransform that moves the view from off screen (imagine a stage to the left or right of the screen) onto the screen while moving the other view in the same direction off-screen. At the end of the function, you can animate your transforms using something like:
UIView.animate(withDuration: animationDuration, animations: moveViewsClosure, completion: cleanUpClosure)
where animationDuration specifies how long this will take, moveViewsClosure is applying the transforms, and cleanUpClosure is actually replacing the views with one-another.
Once you have the class conforming to UIViewControllerAnimatedTransitioning you should be able to return it as the output of the UIViewControllerAnimatedTransitioning function.
3) Now that you have your delegate and animation classes created, you can assign the delegate to a UITabViewController within the SwiftUI view we started in. At the top of the struct, I created a variable of type UITabViewController and used the default initializer. Within the init function, you should set the delegate to an instance of the delegate class we created above.
4) Now we can implement the makeUIViewController and the updateUIViewController functions. Within makeUIViewController you'll need to load the array of views using the UIHostingController to enable your SwiftUI views to sit in the UIKit view controller. Once you have all your views loaded, you can return the UITabViewController from the top. Within the updateUIViewController, you will likely need to reset the delegate to the view controller. Because SwiftUI is using structs, it is not uncommon for your view to get recreated as it is updated; I have found it will lose the reference to the delegate as a result and this was how I solved it.
I realize I could have provided all of my code, but it's fairly lengthy and I think you'll have an easier time troubleshooting it if you understand the process.