I have an issue with Xcode 12 / iOS 14. Using multiple NavigationLinks in a sheet with NavigationView leads to NavigationLink entries staying highlighted after going back a page. This is not only a problem with the simulator. See the attached GIF:
Does anybody know how to fix this?
Similar question: SwiftUI - NavigationLink cell in a Form stays highlighted after detail pop (but that's not the problem here).
struct ContentView: View {
var body: some View {
Text("")
.sheet(isPresented: .constant(true), content: {
NavigationView {
Form {
Section {
NavigationLink("Link to ViewB", destination: ViewB())
}
}
.navigationBarTitle("ViewA")
}
})
}
}
struct ViewB: View {
#State var selection = 0
let screenOptions = ["a", "b", "c"]
var body: some View{
Form {
Section {
NavigationLink("Link to ViewC", destination: ViewC())
}
}
.navigationBarTitle("ViewB")
}
}
struct ViewC: View {
var body: some View{
Form {
Section {
Text("Test")
}
}
.navigationBarTitle("ViewC")
}
}
I've also run into this problem when using a NavigationLink inside a sheet. My solution on iOS 14 has been too Swizzle didSelectRowAt: of UITableView. When the row is selected, I deselect it. There is more code for detecting if its in a sheet, etc, but this is the basic, get it working code:
extension UITableView {
#objc static func swizzleTableView() {
guard self == UITableView.self else {
return
}
let originalTableViewDelegateSelector = #selector(setter: self.delegate)
let swizzledTableViewDelegateSelector = #selector(self.nsh_set(delegate:))
let originalTableViewMethod = class_getInstanceMethod(self, originalTableViewDelegateSelector)
let swizzledTableViewMethod = class_getInstanceMethod(self, swizzledTableViewDelegateSelector)
method_exchangeImplementations(originalTableViewMethod!,
swizzledTableViewMethod!)
}
#objc open func nsh_set(delegate: UITableViewDelegate?) {
nsh_set(delegate: delegate)
guard let delegate = delegate else { return }
let originalDidSelectSelector = #selector(delegate.tableView(_:didSelectRowAt:))
let swizzleDidSelectSelector = #selector(self.tableView(_:didSelectRowAt:))
let swizzleMethod = class_getInstanceMethod(UITableView.self, swizzleDidSelectSelector)
let didAddMethod = class_addMethod(type(of: delegate), swizzleDidSelectSelector, method_getImplementation(swizzleMethod!), method_getTypeEncoding(swizzleMethod!))
if didAddMethod {
let didSelectOriginalMethod = class_getInstanceMethod(type(of: delegate), NSSelectorFromString("tableView:didSelectRowAt:"))
let didSelectSwizzledMethod = class_getInstanceMethod(type(of: delegate), originalDidSelectSelector)
if didSelectOriginalMethod != nil && didSelectSwizzledMethod != nil {
method_exchangeImplementations(didSelectOriginalMethod!, didSelectSwizzledMethod!)
}
}
}
#objc open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tableView(tableView, didSelectRowAt: indexPath)
// This is specifically to fix a bug in SwiftUI, where a NavigationLink is
// not de-selecting itself inside a sheet.
tableView.deselectRow(at: indexPath,
animated: true)
}
}
(Original swizzle code is from https://stackoverflow.com/a/59262109/127853), this code sample just adds the deselectRow call.)
Don't forget to call UITableView.swizzleTableView() somewhere such as application:didFinishLaunchingWithOptions:
Add the following modifier to your NavigationView to set navigation view style and fix this issue:
.navigationViewStyle(StackNavigationViewStyle())
Explanation:
Default style is DefaultNavigationViewStyle(), from documentation: "The default navigation view style in the current context of the view being styled".
For some reason this will pick up DoubleColumnNavigationViewStyle instead of StackNavigationViewStyle on iPhone, if you set style explicitly it behaves as expected.
Related
Have a UIKit Navigation Controller that is the root view controller in the hierarchy of the storyboards. One of the button items navigates to a UIHostedViewController that produces a SwiftUI View. That SwiftUI View is in and of itself a Tab View with tabs - and all of it works fine.
One of the crux of our development is displaying large data models for the user, and we could regain a bit of screen real estate if we utilize the Navigation bar of the UIKit Navigation.
A defining attribute of our individual views is the color coding of the header and footer, we have been able to change these colors easily as the SwiftUI Tab View just calls different views with different modifiers.
What we would like to do is have the selection of the SwiftUI Tab View set a shared property with the UIKit view that would then change the navigation bar color - however the way we setup a bound variable and reload the NavigationBar setup with a didSet on that variable does not seem to be working.
The code is almost exactly as follows, less a couple of views.
class SomeViewController: UIHostingController< MySwiftUITabView > {
var headerColor: Binding<UIColor> = .constant(UIColor.blue) {
didSet {
DispatchQueue.main.async(qos: .userInteractive) {
self.setupUI()
}
}
}
var defaultColor: UIColor!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder, rootView: MySwiftUITabView(headerColor: headerColor))
}
override func viewDidLoad() {
super.viewDidLoad()
defaultColor = self.navigationController?.navigationBar.backgroundColor
setupUI()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
teardownUI()
}
func setupUI() {
if let naviController = self.navigationController {
let newNavBarAppearance = UINavigationBarAppearance()
newNavBarAppearance.backgroundColor = headerColor.wrappedValue
naviController.navigationBar.scrollEdgeAppearance = newNavBarAppearance
naviController.navigationBar.compactAppearance = newNavBarAppearance
naviController.navigationBar.standardAppearance = newNavBarAppearance
}
}
func teardownUI() {
if let naviController = self.navigationController {
let oldNavBarAppearance = UINavigationBarAppearance()
oldNavBarAppearance.backgroundColor = defaultColor
naviController.navigationBar.scrollEdgeAppearance = oldNavBarAppearance
naviController.navigationBar.compactAppearance = oldNavBarAppearance
naviController.navigationBar.standardAppearance = oldNavBarAppearance
}
}
}
And the SwiftUI Tab View:
struct MySwiftUITabView: View {
#Binding var headerColor: UIColor
var body: some View {
TabView(selection: $selectedTab){
TestView()
.tag(0)
.onAppear {
headerColor = .orange
}
.tabItem {
Label("Test", image: "circle.fill")
}
TestView()
.tag(1)
.onAppear {
headerColor = .red
}
.tabItem {
Label("Test", image: "square.fill")
}
}
.accentColor(.white)
}
}
Would a callback be more appropriate for this? We wrapped the didSet function call in a DispatchGroup as a final attempt since it is updating the UI, but the was not effective still for what we are looking for.
In this example, the navigation bar remains the same color as the original set value (.blue) and never gets updated to the color set to the binding by the .onAppear of the swiftUI tab items.
Any help would be greatly appreciated as always!
I've created this very simple wrapper with UIViewControllerRepresentable:
struct ViewControllerWrapperView: UIViewControllerRepresentable {
let controller: UIViewController
func makeUIViewController(context: UIViewControllerRepresentableContext<ViewControllerWrapperView>) -> UIViewController {
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ViewControllerWrapperView>) {}
}
Using it to display ViewControllers in a SwiftUI view works fine:
struct ContentView: View {
#State private var viewSwitch: Bool = true
let blueView: ViewControllerWrapperView = {
let blueViewController = UIViewController()
blueViewController.view.backgroundColor = .blue
return ViewControllerWrapperView(controller: blueViewController)
}()
let redView: ViewControllerWrapperView = {
let redViewController = UIViewController()
redViewController.view.backgroundColor = .red
return ViewControllerWrapperView(controller: redViewController)
}()
var body: some View {
Button("Switch") { viewSwitch.toggle() }
if viewSwitch {
blueView
} else {
redView
}
}
}
But as soon as I wrap the ViewControllerWrapperViews in AnyView they stop working properly:
struct ContentView: View {
#State private var viewSwitch: Bool = true
let blueView: AnyView = {
let blueViewController = UIViewController()
blueViewController.view.backgroundColor = .blue
return AnyView(ViewControllerWrapperView(controller: blueViewController))
}()
let redView: AnyView = {
let redViewController = UIViewController()
redViewController.view.backgroundColor = .red
return AnyView(ViewControllerWrapperView(controller: redViewController))
}()
var body: some View {
Button("Switch") { viewSwitch.toggle() }
if viewSwitch {
blueView
} else {
redView
}
}
}
With AnyView the views don't switch when the button is tapped. Looking a bit deeper into it, I discovered the following:
For both scenarios when first displaying the view:
makeUIViewController is called on the ViewControllerWrapperView for the blue view.
updateUIViewController is called on the ViewControllerWrapperView for the blue view and the parameter uiViewController is the blue ViewController.
Now without AnyView when the switch button is tapped the life cycle of the UIViewControllerRepresentable is executed as supposed to:
updateUIViewController is called on the ViewControllerWrapperView for the blue view and the parameter uiViewController is the blue ViewController.
makeUIViewController is called on the ViewControllerWrapperView for the red view.
updateUIViewController is called on the ViewControllerWrapperView for the red view and the parameter uiViewController is the red ViewController.
dismantleUIViewController is called and the parameter uiViewController is the blue view.
But with AnyView when tapping the switch button, the only thing that happens is:
updateUIViewController is called on the ViewControllerWrapperView for the RED view and the parameter uiViewController is the BLUE ViewController.
Am I missing something or is this a bug in SwiftUI?
Am I missing something or is this a bug in SwiftUI?
It is not a bug, AnyView erases type differences, so rendering body SwiftUI engine sees only AnyView replaced with AnyView which are equal, so engine does not replace existed view, but just refreshes it (because state has been changed) that results in updateUIViewController call. All is as expected.
And that's why usage of AnyView should be very very careful and meaningful with clear understanding of process and consequences.
A Section may contain 1 header, many content items and 1 footer.
For DiffableDataSource, most of the online examples, are using enum to represent Section. For instance
func applySnapshot(_ animatingDifferences: Bool) {
var snapshot = Snapshot()
snapshot.appendSections([.MainAsEnum])
snapshot.appendItems(filteredTabInfos, toSection: .MainAsEnum)
dataSource?.apply(snapshot, animatingDifferences: animatingDifferences)
}
However, when the Section has a dynamic content footer, we may need to use struct to represent Section. For instance
import Foundation
struct TabInfoSection {
// Do not include content items [TabInfo] as member of Section. If not, any mutable
// operation performed on content items, will misguide Diff framework to throw
// away entire current Section, and replace it with new Section. This causes
// flickering effect.
var footer: String
}
extension TabInfoSection: Hashable {
}
But, how are we suppose to update only footer?
The current approach provided by
DiffableDataSource: Snapshot Doesn't reload Headers & footers is not entirely accurate
If I try to update footer
class TabInfoSettingsController: UIViewController {
…
func applySnapshot(_ animatingDifferences: Bool) {
var snapshot = Snapshot()
let section = tabInfoSection;
snapshot.appendSections([section])
snapshot.appendItems(filteredTabInfos, toSection: section)
dataSource?.apply(snapshot, animatingDifferences: animatingDifferences)
}
var footerValue = 100
extension TabInfoSettingsController: TabInfoSettingsItemCellDelegate {
func crossButtonClick(_ sender: UIButton) {
let hitPoint = (sender as AnyObject).convert(CGPoint.zero, to: collectionView)
if let indexPath = collectionView.indexPathForItem(at: hitPoint) {
// use indexPath to get needed data
footerValue = footerValue + 1
tabInfoSection.footer = String(footerValue)
//
// Perform UI updating.
//
applySnapshot(true)
}
}
}
I will get the following flickering outcome.
The reason of flickering is that, the diff framework is throwing entire old Section, and replace it with new Section, as it discover there is change in TabInfoSection object.
Is there a good way, to update footer in Section via DiffableDataSource without causing flickering effect?
p/s The entire project source code can be found in https://github.com/yccheok/ios-tutorial/tree/broken-demo-for-footer-updating under folder TabDemo.
Have you thought about making a section only for the footer? So that way there's no reload, when it flickers, since it's technically not apart of the problematic section?
There is a fast fix for it, but you will loose the animation of the tableview. In TabInfoSettingsController.swift you can force false the animations in this function:
func applySnapshot(_ animatingDifferences: Bool) {
var snapshot = Snapshot()
let section = tabInfoSection;
snapshot.appendSections([section])
snapshot.appendItems(filteredTabInfos, toSection: section)
dataSource?.apply(snapshot, animatingDifferences: false)
}
You will not see the flickering effect but you will loose the standard animation.
if you want to update only collectionview footer text then make it variable of TabInfoSettingsFooterCell.
var tableSection: TabInfoSettingsFooterCell?
DataSource
func makeDataSource() -> DataSource {
let dataSource = DataSource(
collectionView: collectionView,
cellProvider: { (collectionView, indexPath, tabInfo) -> UICollectionViewCell? in
guard let tabInfoSettingsItemCell = collectionView.dequeueReusableCell(
withReuseIdentifier: TabInfoSettingsController.tabInfoSettingsItemCellClassName,
for: indexPath) as? TabInfoSettingsItemCell else {
return nil
}
tabInfoSettingsItemCell.delegate = self
tabInfoSettingsItemCell.reorderDelegate = self
tabInfoSettingsItemCell.textField.text = tabInfo.getPageTitle()
return tabInfoSettingsItemCell
}
)
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
guard kind == UICollectionView.elementKindSectionFooter else {
return nil
}
let section = dataSource.snapshot().sectionIdentifiers[indexPath.section]
guard let tabInfoSettingsFooterCell = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: TabInfoSettingsController.tabInfoSettingsFooterCellClassName,
for: indexPath) as? TabInfoSettingsFooterCell else {
return nil
}
tabInfoSettingsFooterCell.label.text = section.footer
//set tableSection value
self.tableSection = tabInfoSettingsFooterCell
return tabInfoSettingsFooterCell
}
return dataSource
}
TabInfoSettingsItemCellDelegate
func crossButtonClick(_ sender: UIButton) {
let hitPoint = (sender as AnyObject).convert(CGPoint.zero, to: collectionView)
if let indexPath = collectionView.indexPathForItem(at: hitPoint) {
footerValue = footerValue + 1
tabInfoSection.footer = String(footerValue)
//Update section value
self.tableSection?.label.text = String(footerValue)
}
}
It might sound like a trivial task but I can't find a proper solution for this problem. Possibly I haven't internalized the "SwiftUI-ish" way of thinking yet.
I have a view with a button. When the view loads, there is a condition (already logged in?) under which the view should directly go to the next view. If the button is clicked, an API call is triggered (login) and if it was successful, the redirect to the next view should also happen.
My attempt was to have a model (ObservableObject) that holds the variable "shouldRedirectToUploadView" which is a PassThroughObject. Once the condition onAppear in the view is met or the button is clicked (and the API call is successful), the variable flips to true and tells the observer to change the view.
Flipping the "shouldRedirectToUploadView" in the model seems to work but I can't make the view re-evaluate that variable so the new view won't open.
Here is my implementation so far:
The model
import SwiftUI
import Combine
class SboSelectorModel: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
var shouldRedirectToUpdateView = false {
didSet {
didChange.send()
}
}
func fetch(_ text: String) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.shouldRedirectToUpdateView = true
}
}
}
The view
import SwiftUI
struct SboSelectorView: View {
#State var text: String = ""
#ObservedObject var model: SboSelectorModel
var body: some View {
return ZStack {
if (model.shouldRedirectToUpdateView) {
UpdateView()
}
else {
Button(action: {
self.reactOnButtonClick()
}) {
Text("Start")
}
}
}.onAppear(perform: initialActions)
}
public func initialActions() {
self.model.shouldRedirectToUpdateView = true
}
private func reactOnButtonClick() {
self.model.fetch()
}
}
In good old UIKit I would have just used a ViewController to catch the action of button click and then put the new view on the navigation stack. How would I do it in SwiftUI?
In the above example I would expect the view to load, execute the onAppear() function which executes initialActions() to flip the model variable what would make the view react to that change and present the UploadView. Why doesn't it happen that way?
There are SO examples like Programatically navigate to new view in SwiftUI or Show a new View from Button press Swift UI or How to present a view after a request with URLSession in SwiftUI? that suggest the same procedure. However it does not seem to work for me. Am I missing something?
Thank you in advance!
Apple has introduced #Published which does all the model did change stuff.
This works for me and it looks much cleaner.
You can also use .onReceive() to perform stuff on a view when something in your model changes.
class SboSelectorModel: ObservableObject {
#Published var shouldRedirectToUpdateView = false
func fetch(_ text: String) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.shouldRedirectToUpdateView = true
}
}
}
struct UpdateView: View {
var body: some View {
Text("Hallo")
}
}
struct SboSelectorView: View {
#State var text: String = ""
#ObservedObject var model = SboSelectorModel()
var body: some View {
ZStack {
if (self.model.shouldRedirectToUpdateView) {
UpdateView()
}
else {
Button(action: {
self.reactOnButtonClick()
}) {
Text("Start")
}
}
}.onAppear(perform: initialActions)
}
public func initialActions() {
self.model.shouldRedirectToUpdateView = true
}
private func reactOnButtonClick() {
self.model.fetch("")
}
}
I hope this helps.
EDIT
So this seems to have changed in beta 5
Here a working model with PassthroughSubject:
class SboSelectorModel: ObservableObject {
let objectWillChange = PassthroughSubject<Bool, Never>()
var shouldRedirectToUpdateView = false {
willSet {
objectWillChange.send(shouldRedirectToUpdateView)
}
}
func fetch(_ text: String) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.shouldRedirectToUpdateView = true
}
}
}
Just in case any wants an alternative to the SwiftUI way of having Observable Objects and the like - which can be great, but as I was building out, I noticed I had like, 100 objects and didn't like in the slightest how complicated it all felt. (Oh, how I wanted to just type self.present("nextScene", animated: true)). I know a large part of this is my mind just not up to that SwiftUI life yet but just in case anyone else wants a more... UIKit meets SwiftUI alternative, here's a system that works.
I'm not a professional so I don't know if this is the best memory management way.
First, create a function that allows you to know what the top view controller is on the screen. The code below was borrowed from db0Company on GIT.
import UIKit
extension UIViewController {
func topMostViewController() -> UIViewController {
if let presented = self.presentedViewController {
return presented.topMostViewController()
}
if let navigation = self.presentedViewController as? UINavigationController {
return navigation.visibleViewController?.topMostViewController() ?? navigation
}
if let tab = self as? UITabBarController {
return tab.selectedViewController?.topMostViewController() ?? tab
}
return self
}
}
extension UIApplication {
func topMostViewController() -> UIViewController? {
return self.keyWindow?.rootViewController?.topMostViewController()
}
}
Create an enum - now this is optional, but I think very helpful - of your SwiftUI and UIViewControllers; for demonstration purposes, I have 2.
enum RootViews {
case example, welcome
}
Now, here's some fun; create a delegate you can call from your SwiftUI views to move you from scene to scene. I call mine Navigation Delegate.
I added some default presentation styles here, to make calls easier via the extension.
import UIKit //SUPER important!
protocol NavigationDelegate {
func moveTo(view: RootViews, presentation: UIModalPresentationStyle, transition: UIModalTransitionStyle)
}
extension NavigationDelegate {
func moveTo(view: RootViews) {
self.moveTo(view: view, presentation: .fullScreen, transition: .crossDissolve)
}
func moveTo(view: RootViews, presentation: UIModalPresentationStyle) {
self.moveTo(view: view, presentation: presentation, transition: .crossDissolve)
}
func moveTo(view: RootViews, transition: UIModalTransitionStyle) {
self.moveTo(view: view, presentation: .fullScreen, transition: transition)
}
}
And here, I create a RootViewController - a classic, Cocoa Touch Class UIViewController. This will conform to the delegate, and be where we actually move screens.
class RootViewController: UIViewController, NavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.moveTo(view: .welcome) //Which can always be changed
}
//The Moving Function
func moveTo(view: RootViews, presentation: UIModalPresentationStyle = .fullScreen, transition: UIModalTransitionStyle = .crossDissolve) {
let newScene = self.returnSwiftUIView(type: view)
newScene.modalPresentationStyle = presentation
newScene.modalTransitionStyle = transition
//Top View Controller
let top = self.topMostViewController()
top.present(newScene, animated: true)
}
//Swift View switch. Optional, but my Xcode was not happy when I tried to return a UIHostingController in line.
func returnSwiftUIView(type: RootViews) -> UIViewController {
switch type {
case .welcome:
return UIHostingController(rootView: WelcomeView(delegate: self))
case .example:
return UIHostingController(rootView: ExampleView(delegate: self))
}
}
}
So now, when you create new SwiftUI Views, you just need to add the Navigation Delegate, and call it when a button is pressed.
import SwiftUI
import UIKit //Very important! Don't forget to import UIKit
struct WelcomeView: View {
var delegate: NavigationDelegate?
var body: some View {
Button(action: {
print("full width")
self.delegate?.moveTo(view: .name)
}) {
Text("NEXT")
.frame(width: UIScreen.main.bounds.width - 20, height: 50, alignment: .center)
.background(RoundedRectangle(cornerRadius: 15, style: .circular).fill(Color(.systemPurple)))
.accentColor(.white)
}
}
And last but not least, in your scene delegate, create your RootViewController() and use that as your key, instead of the UIHostingController(rootView: contentView).
Voila.
I hope this can help someone out there! And for my more professional senior developers out there, if you can see a way to make it... cleaner? Or whatever it is that makes code less bad, feel free!
Using a List view, is there a way to access (and therefore modify) the underlying UITableView object without reimplementing the entire List as a UIViewRepresentable?
I've tried initializing a List within my own UIViewRepresentable, but I can't seem to get SwiftUI to initialize the view when I need it to, and I just get an empty basic UIView with no subviews.
This question is to help find an answer for Bottom-first scrolling in SwiftUI.
Alternatively, a library or other project that reimplements UITableView in SwiftUI would also answer this question.
The answer is Yes. There's an amazing library that lets you inspect the underlying UIKit views. Here's a link to it.
The answer is no. As of iOS 13, SwiftUI's List is not currently designed to replace all the functionality and customizability of UITableView. It is designed to meet the most basic use of a UITableView: a standard looking, scrollable, editable list where you can place a relatively simply view in each cell.
In other words, you are giving up customizability for the simplicity of having swipes, navigation, moves, deletes, etc. automatically implemented for you.
I'm sure that as SwiftUI evolves, List (or an equivalent view) will get more customizable, and we'll be able to do things like scroll from the bottom, change padding, etc. The best way to make sure this happens is to file feedback suggestions with Apple. I'm sure the SwiftUI engineers are already hard at work designing the features that will appear at WWDC 2020. The more input they have to guide what the community wants and needs, the better.
I found a library called Rotoscope on GitHub (I am not the author of this).
This library is used to implement RefreshUI also on GitHub by the same author.
How it works is that Rotoscope has a tagging method, which overlays a 0 sized UIViewRepresentable on top of your List (so it's invisible). The view will dig through the chain of views and eventually find the UIHostingView that's hosting the SwiftUI views. Then, it will return the first subview of the hosting view, which should contains a wrapper of UITableView, then you can access the table view object by getting the subview of the wrapper.
The RefreshUI library uses this library to implement a refresh control to the SwiftUI List (you can go into the GitHub link and check out the source to see how it's implemented).
However, I see this more like a hack than an actual method, so it's up to you to decide whether you want to use this or not. There are no guarantee that it will continue working between major updates as Apple could change the internal view layout and this library will break.
You can Do it. But it requires a Hack.
Add Any custom UIView
Use UIResponder to backtrack until you find table View.
Modify UITableView The way you like.
Code Example of Adding Pull to refresh:
//1: create a custom view
final class UIKitView : UIViewRepresentable {
let callback: (UITableView) -> Void
init(leafViewCB: #escaping ((UITableView) -> Void)) {
callback = leafViewCB
}
func makeUIView(context: Context) -> UIView {
let view = UIView.init(frame: CGRect(x: CGFloat.leastNormalMagnitude,
y: CGFloat.leastNormalMagnitude,
width: CGFloat.leastNormalMagnitude,
height: CGFloat.leastNormalMagnitude))
view.backgroundColor = .clear
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
if let superView = uiView.superview {
superView.backgroundColor = uiView.backgroundColor
}
if let tableView = uiView.next(UITableView.self) {
callback(tableView)
}
}
}
extension UIResponder {
func next<T: UIResponder>(_ type: T.Type) -> T? {
return next as? T ?? next?.next(type)
}
}
////Use:
struct Result: Identifiable {
var id = UUID()
var value: String
}
class RefreshableObject: ObservableObject {
let id = UUID()
#Published var items: [Result] = [Result(value: "Binding"),
Result(value: "ObservableObject"),
Result(value: "Published")]
let refreshControl: UIRefreshControl
init() {
refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action:
#selector(self.handleRefreshControl),
for: .valueChanged)
}
#objc func handleRefreshControl(sender: UIRefreshControl) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
sender.endRefreshing()
self?.items = [Result(value:"new"), Result(value:"data"), Result(value:"after"), Result(value:"refresh")]
}
}
}
struct ContentView: View {
#ObservedObject var refreshableObject = RefreshableObject()
var body: some View {
NavigationView {
Form {
Section(footer: UIKitView.init { (tableView) in
if tableView.refreshControl == nil {
tableView.refreshControl = self.refreshableObject.refreshControl
}
}){
ForEach(refreshableObject.items) { result in
Text(result.value)
}
}
}
.navigationBarTitle("Nav bar")
}
}
}
Screenshot:
To update from refresh action, binding isUpdateOrdered is being used.
this code is based on code I found in web, couldn't find the author
import Foundation
import SwiftUI
class Model: ObservableObject{
#Published var isUpdateOrdered = false{
didSet{
if isUpdateOrdered{
update()
isUpdateOrdered = false
print("we got him!")
}
}
}
var random = 0
#Published var arr = [Int]()
func update(){
isUpdateOrdered = false
//your update code.... maybe some fetch request or POST?
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
NavigationView {
LegacyScrollViewWithRefresh(isUpdateOrdered: $model.isUpdateOrdered) {
VStack{
if model.arr.isEmpty{
//this is important to fill the
//scrollView with invisible data,
//in other case scroll won't work
//because of the constraints.
//You may get rid of them if you like.
Text("refresh!")
ForEach(1..<100){ _ in
Text("")
}
}else{
ForEach(model.arr, id:\.self){ i in
NavigationLink(destination: Text(String(i)), label: { Text("Click me") })
}
}
}
}.environmentObject(model)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct LegacyScrollViewWithRefresh: UIViewRepresentable {
enum Action {
case idle
case offset(x: CGFloat, y: CGFloat, animated: Bool)
}
typealias Context = UIViewRepresentableContext<Self>
#Binding var action: Action
#Binding var isUpdateOrdered: Bool
private let uiScrollView: UIScrollView
private var uiRefreshControl = UIRefreshControl()
init<Content: View>(isUpdateOrdered: Binding<Bool>, content: Content) {
let hosting = UIHostingController(rootView: content)
hosting.view.translatesAutoresizingMaskIntoConstraints = false
self._isUpdateOrdered = isUpdateOrdered
uiScrollView = UIScrollView()
uiScrollView.addSubview(hosting.view)
let constraints = [
hosting.view.leadingAnchor.constraint(equalTo: uiScrollView.leadingAnchor),
hosting.view.trailingAnchor.constraint(equalTo: uiScrollView.trailingAnchor),
hosting.view.topAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.topAnchor),
hosting.view.bottomAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.bottomAnchor),
hosting.view.widthAnchor.constraint(equalTo: uiScrollView.widthAnchor)
]
uiScrollView.addConstraints(constraints)
self._action = Binding.constant(Action.idle)
}
init<Content: View>(isUpdateOrdered: Binding<Bool>, #ViewBuilder content: () -> Content) {
self.init(isUpdateOrdered: isUpdateOrdered, content: content())
}
init<Content: View>(isUpdateOrdered: Binding<Bool>, action: Binding<Action>, #ViewBuilder content: () -> Content) {
self.init(isUpdateOrdered: isUpdateOrdered, content: content())
self._action = action
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIScrollView {
uiScrollView.addSubview(uiRefreshControl)
uiRefreshControl.addTarget(context.coordinator, action: #selector(Coordinator.handleRefreshControl(arguments:)), for: .valueChanged)
return uiScrollView
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
switch self.action {
case .offset(let x, let y, let animated):
uiView.setContentOffset(CGPoint(x: x, y: y), animated: animated)
DispatchQueue.main.async {
self.action = .idle
}
default:
break
}
}
class Coordinator: NSObject {
let legacyScrollView: LegacyScrollViewWithRefresh
init(_ legacyScrollView: LegacyScrollViewWithRefresh) {
self.legacyScrollView = legacyScrollView
}
#objc func handleRefreshControl(arguments: UIRefreshControl){
print("refreshing")
self.legacyScrollView.isUpdateOrdered = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2){
arguments.endRefreshing()
//refresh animation will
//always be shown for 2 seconds,
//you may connect this behaviour
//to your update completion
}
}
}
}
There is currently no way to access or modify the underlying UITableView