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.
Related
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.
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
This is from Apple documentation on the SwiftUI App protocol:
#main
struct Mail: App {
#StateObject private var model = MailModel()
var body: some Scene {
WindowGroup {
MailViewer()
.environmentObject(model) // Passed through the environment.
}
Settings {
SettingsView(model: model) // Passed as an observed object.
}
}
}
Why do we need to use the #StateObject propertyWrapper in this case? Why isn't a normal property enough?
I suspect that the "App" struct is a configuration object, just like views in SwiftUI? And that we can't count on that struct to hang around once the body has been read? Correct?
The main reason is for previews, models that are initialised with #StateObject are not initialised. WWDC 2020 Structure your app for SwiftUI previews
8:19
Secondly, the model changes, #StateObject will allow SwiftUI to detect the change and cause the body to be re-computed because model is referenced (twice) within the body, which SwiftUI knows by dependency tracking. This means that MailViewer and SettingsView will be re-created with the new data in the model. Then if anything changed in those View structs, SwiftUI will detect that by diffing the new structs from the ones returned previously and update the screen with whatever changes are needed to bring the screen up to date.
As you say, we can't guarantee structs to hang around, in fact they do not, they are created, the screen is rendered and they are thrown away. That is why we use property wrappers so when the struct is created again it is given the same data to use for the property. In the case of #StateObject the object is created once, the first time the body is computed of the first struct. If a struct is no longer being created, e.g. it is excluded by an if statement, then the object is deinit. If the struct is created again in the future then a new object is created, this is more of a feature for Views than for Apps. This means that the state object's life cycle is tied to the life cycle of the View being shown on screen which is very powerful.
If we were to use normal properties to init objects on SwiftUI structs then those objects would be created every time a struct is re-created which is a heap allocation which fills up RAM and slows down SwiftUI and should be avoided at all costs.
According to Apple #StateObject guaranties that model in
#StateObject private var model = MailModel()
will be created only once. That's all difference from #ObservedObject. So if that is not important for you (or not the case) and you don't need to observe it at that level then you can use regular property declaration.
In UIKit-based application we can have custom navigation transitions using UIViewControllerAnimatedTransitioning protocol.
Is there an equivalent in SwiftUI?
I know we can already animate between removing and adding views.
But how do we do this when we push and pop to the navigation stack?
There isn't anything like this available in SwiftUI's APIs so far afaik. Make sure to open an enhancement request with Apple.
Implementing "navigation" on your own is a terrible idea, as you basically give all the accessibility and facility support afforded by UINavigationController.
Instead, here is a suggestion:
Either by means of a custom SwiftUI view or modifier, wrap the NavigationView with a UIViewControllerRepresentable wrapper, which in turn sets up a UIHostingController subclass, which waits for addChild(_:) to be called, which will be the navigation controller added as a child of the hosting controller. From here, if your animations are "static" (e.g. do not require any subview to subview transition), you can accomplish this here by implementing the navigation controller delegate, and providing the animation controller.
If you do need more evolved transitions (such as Photos.app's photo transition), you can create a custom UIViewRepresentable wrapper, which will serve as markers for "from" views and "to" views, you can then use those discover in UIKit, and e.g. can snapshot and animate in transitions.
Proposed API can look like this:
struct ContentView1: View {
var body: some View {
NavigationLink(destination: DetailView()) {
Image("small").customSourceTarget()
}.navigationBarTitle("Navigation")
}
}
struct ContentView2: View {
var body: some View {
Image("large").customTransitionTarget()
}
}
NavigationView {
ContentView1()
}.customAnimatable()
I've written a custom class based on UIPickerView. Within a class I implemented next method:
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
let min = (selectedRow(inComponent: 0) % minutes.count)
let sec = (selectedRow(inComponent: 1) % seconds.count)
self.min = min
self.sec = sec
}
Every time user has selected value from "min" and "sec" using picker view, these class variables are changed accordingly. So, it's work ok.
Now, I would like this class to send event every time these variables are changed. Is it possible to adopt valueChanged event for this class, and how to implement it, or I have to make custom event for these purposes? I would like to do it in that way to be able making action outlets via IB. Please, correct me if I missed the pattern.
May be I put this question in other words: what makes the build-in UIDataPicker able to connect to view controller within interface builder with actions outlets - and what should I add to my custom class to achieve partially that functionality?
Apple stated that: "UIDatePicker class uses a custom subclass of UIPickerView to display dates and times." Correct me here if I'm wrong, but they also point out that UIDatePicker inherits from UIControl rather than from UIPickerView. As for UIPickerView - they say it inherits from the UIView. So, I see no connection in terms of inheritance of UIControl for UIPickerView and UIDatePicker. Also I can't inherit UIControl to obtain event sending functionality due to multiple inheritance of UIView class. Can't understand why things here so weird and I can't use UIControl functionality for UIPickerView.
create YourCustomControl inherit from UIControl
addSubview UIPickerView
set YourCustomControl as a delegate for this picker
To send event you need add this line
sendActions(for: .valueChanged)
in every place where these variables are changed
UIKit - sendActions