SWIFTUI refreshable viewModifier - swiftui

For SWIFTUI i have created a ViewModifier to use the refreshable(action) view modifier available under iOS15.
with this modifier I don't have to embed my all view in availability check.
It's seems to work properly, but I'm not confortable with the #Sendable and I wondering if there is some performance issue.
Do you think this is a good implementation?
struct RefreshView: ViewModifier {
var action: #Sendable () async -> Void
func body(content: Content) -> some View {
if #available(iOS 15, *) {
content.refreshable(action: action)
} else {
content
}
}
}
extension View {
func refreshView(action: #escaping #Sendable () async -> Void) -> some View {
modifier(RefreshView(action: action))
}
}

Related

SwiftUI: In SplitView, how can I detect if the master view is visible?

When SwiftUI creates a SplitView, it adds a toolbar button that hides/shows the Master view. How can I detect this change so that I can resize the font in the detail screen and use all the space optimally?
I've tried using .onChange with geometry but can't seem to get that to work.
If you're using iOS 16 you can use NavigationSplitView with NavigationSplitViewVisibility
Example:
struct MySplitView: View {
#State private var columnVisibility: NavigationSplitViewVisibility = .all
var bothAreShown: Bool { columnVisibility != .detailOnly }
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
Text("Master Column")
} detail: {
Text("Detail Column")
Text(bothAreShown ? "Both are shown" : "Just detail shown")
}
}
}
After thinkering for a while on this I got to this solution:
struct ContentView: View {
#State var isOpen = true
var body: some View {
NavigationView {
VStack{
Text("Primary")
.onUIKitAppear {
isOpen.toggle()
}
.onAppear{
print("hello")
isOpen.toggle()
}
.onDisappear{
isOpen.toggle()
print("hello: bye")
}
.navigationTitle("options")
}
Text("Secondary").font(isOpen ? .body : .title)
}.navigationViewStyle(.columns)
}
}
The onUIKitAppear is a custom extension suggested by apple to be only executed once the view has been presented to the user https://developer.apple.com/forums/thread/655338?page=2
struct UIKitAppear: UIViewControllerRepresentable {
let action: () -> Void
func makeUIViewController(context: Context) -> UIAppearViewController {
let vc = UIAppearViewController()
vc.delegate = context.coordinator
return vc
}
func makeCoordinator() -> Coordinator {
Coordinator(action: self.action)
}
func updateUIViewController(_ controller: UIAppearViewController, context: Context) {}
class Coordinator: ActionRepresentable {
var action: () -> Void
init(action: #escaping () -> Void) {
self.action = action
}
func remoteAction() {
action()
}
}
}
protocol ActionRepresentable: AnyObject {
func remoteAction()
}
class UIAppearViewController: UIViewController {
weak var delegate: ActionRepresentable?
var savedView: UIView?
override func viewDidLoad() {
self.savedView = UILabel()
if let _view = self.savedView {
view.addSubview(_view)
}
}
override func viewDidAppear(_ animated: Bool) {
delegate?.remoteAction()
}
override func viewDidDisappear(_ animated: Bool) {
view.removeFromSuperview()
savedView?.removeFromSuperview()
}
}
public extension View {
func onUIKitAppear(_ perform: #escaping () -> Void) -> some View {
self.background(UIKitAppear(action: perform))
}
}

UISearchController how to activate from SwiftUI

I found an integration of the uiSearchController in SwiftUI, but I don't know how to let it become active?
I found this:
I want that the searchBar becomes active when changing an Bool in the SwiftUI View with a #State for example.
If I add a Binding to the view modifier and set the isActive property of the searchController in
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
viewController.navigationItem.hidesSearchBarWhenScrolling = false
}
then is doesn't become active.
Im not really familiar with UIKit, perhaps anybody knows how to correctly activate the searchbar that one can start typing for a search.
class SearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
}
}
extension SearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
final class ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
}
func makeUIViewController(context: Context) -> ParentResolverViewController {
ParentResolverViewController(onResolve: onResolve)
}
func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) {
}
}
class ParentResolverViewController: UIViewController {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use init(onResolve:) to instantiate ParentResolverViewController.")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
onResolve(parent)
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
viewController.navigationItem.hidesSearchBarWhenScrolling = false
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: SearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
To activate a UISearchBar (which is what you're using), just do:
searchController.searchBar.becomeFirstResponder()
(from this answer)
Now all we need to do is reference searchController.searchBar from the SwiftUI view. First, add a function to your SearchBar class.
class SearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
}
/// add this function
func activate() {
searchController.searchBar.becomeFirstResponder()
}
}
Then, just call it. I think this is better than setting a #State, but if you require that, let me know and I'll edit my answer.
struct ContentView: View {
#StateObject var searchBar = SearchBar()
var body: some View {
NavigationView {
Button(action: {
searchBar.activate() /// activate the search bar
}) {
Text("Activate search bar")
}
.modifier(SearchBarModifier(searchBar: searchBar))
.navigationTitle("Navigation View")
}
}
}
Result:

Make UIKit ContextMenu preview provider frame fit a SwiftUI view inside a UIHostingControler

I am learning Swift & SwiftUI as a hobby with no UIKit background, so I am not sure if this is currently possible. I would really like to use UIKit's context menus with SwiftUI (e.g. to implement submenus, action attributes and maybe custom preview providers).
My original idea was to create a LegacyContextMenuView with UIViewControllerRepresentable. Then I'd use a UIHostingController to add a SwiftUI View as a child of a UIViewController ContainerViewController to which I'd add a UIContextMenuInteraction.
My current solution kinda works but when the context menu is activated the preview frame of the 'ContainerViewController' view does not fit the size of UIHostingController view. I am not familiar with UIKit's layout system so I'd like to know:
Is it possible to add such constrains while the preview is activated?
Is it possible to preserve the clipShape of the underlying SwiftUI view inside the preview provider?
The code:
// MARK: - Describes a UIKit Context Menu
struct LegacyContextMenu {
let title: String
let actions: [UIAction]
var actionProvider: UIContextMenuActionProvider {
{ _ in
UIMenu(title: title, children: actions)
}
}
init(actions: [UIAction], title: String = "") {
self.actions = actions
self.title = title
}
}
// MARK: - A View that brings UIKit context menus into the SwiftUI world
struct LegacyContextMenuView<Content: View>: UIViewControllerRepresentable {
let content: Content
let menu: LegacyContextMenu
func makeUIViewController(context: Context) -> UIViewController {
let controller = ContainerViewController(rootView: content)
let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator)
controller.view.addInteraction(menuInteraction)
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
class Coordinator: NSObject, UIContextMenuInteractionDelegate {
let parent: LegacyContextMenuView
init(parent: LegacyContextMenuView) { self.parent = parent }
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration?
{
// previewProvider nil = using the default UIViewController: ContainerViewController
UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: parent.menu.actionProvider)
}
}
class ContainerViewController: UIViewController {
let hostingController: UIHostingController<Content>
init(rootView: Content) {
self.hostingController = UIHostingController(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
setupHostingController()
setupContraints()
// Additional setup required?
}
func setupHostingController() {
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
}
// Not familiar with UIKit's layout system so unsure if this is the best approach
func setupContraints() {
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints([
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostingController.view.leftAnchor.constraint(equalTo: view.leftAnchor),
hostingController.view.rightAnchor.constraint(equalTo: view.rightAnchor)
])
}
}
}
// MARK: - Simulate SwiftUI syntax
extension View {
func contextMenu(_ legacyContextMenu: LegacyContextMenu) -> some View {
self.modifier(LegacyContextViewModifier(menu: legacyContextMenu))
}
}
struct LegacyContextViewModifier: ViewModifier {
let menu: LegacyContextMenu
func body(content: Content) -> some View {
LegacyContextMenuView(content: content, menu: menu)
}
}
Then to test, I use this:
// MARK - A sample view with custom content shape and a dynamic frame
struct SampleView: View {
#State private var isLarge = false
let viewClipShape = RoundedRectangle(cornerRadius: 50.0)
var body: some View {
ZStack {
Color.blue
Text(isLarge ? "Large" : "Small")
.foregroundColor(.white)
.font(.largeTitle)
}
.onTapGesture { isLarge.toggle() }
.clipShape(viewClipShape)
.contentShape(viewClipShape)
.frame(height: isLarge ? 250 : 150)
.animation(.easeInOut, value: isLarge)
}
}
struct ContentView: View {
var body: some View {
SampleView()
.contextMenu(LegacyContextMenu(actions: [sampleAction], title: "My Menu"))
.padding(.horizontal)
}
let sampleAction = UIAction(
title: "Remove",
image: UIImage(systemName: "trash.fill"),
identifier: nil,
attributes: UIMenuElement.Attributes.destructive,
handler: { _ in print("Pressed 'Remove'") })
}
While long pressing, the context menu scaling animation respects the content shape of SampleView for both small and large sizes, but the preview pops out like this:
you must set preferredContentSize of ViewController to fit content size u want

Conditional modifiers with #available not executing correctly

I've got a custom modifier to replace navigation bar title with an image view, in iOS 14 this is pretty straightforward with the .toolbar modifier, however in iOS 13 it needs a bit more work but it's possible.
The problem comes when I want to use both solutions in a conditional modifier, the following code reproduces the issue, it works when running on iOS 14 but it produces no result on iOS 13, however if the "#available" condition is removed from the modifier leaving only iOS 13 code, it works as expected. Wrapping iOS 14 in AnyView does not help either:
extension View {
#ViewBuilder
func configuresIcon() -> some View {
if #available(iOS 14.0, *){
self.modifier(NavigationConfigurationView14Modifier())
} else {
self.modifier(NavigationConfigurationViewModifier(configure: { nv in
nv.topItem?.titleView = UIImageView(image: UIImage(named: IMAGE_NAME_HERE))
}))
}
}
}
struct NavigationConfigurationViewModifier: ViewModifier {
let configure: (UINavigationBar) -> ()
func body(content: Content) -> some View {
content.background(NavigationControllerLayout(configure: {
configure($0.navigationBar)
}))
}
}
#available(iOS 14.0, *)
struct NavigationConfigurationView14Modifier: ViewModifier {
func body(content: Content) -> some View {
content
.toolbar {
ToolbarItem(placement: .principal) {
Image(IMAGE_NAME_HERE)
}
}
}
}
struct NavigationControllerLayout: UIViewControllerRepresentable {
var configure: (UINavigationController) -> () = { _ in }
func makeUIViewController(
context: UIViewControllerRepresentableContext<NavigationControllerLayout>
) -> UIViewController {
UIViewController()
}
func updateUIViewController(
_ uiViewController: UIViewController,
context: UIViewControllerRepresentableContext<NavigationControllerLayout>
) {
if let navigationContoller = uiViewController.navigationController {
configure(navigationContoller)
}
}
}
Try to wrap content in Group (not tested - only idea)
extension View {
#ViewBuilder
func configuresIcon() -> some View {
Group {
if #available(iOS 14.0, *){
self.modifier(NavigationConfigurationView14Modifier())
} else {
self.modifier(NavigationConfigurationViewModifier(configure: { nv in
nv.topItem?.titleView = UIImageView(image: UIImage(named: IMAGE_NAME_HERE))
}))
}
}
}
}
Maybe it will work if we check OS version at runtime, not at compiletime (because buildlimitedavailability(_:) is itself available only since iOS 14).
extension View {
func erase() -> AnyView {
return AnyView(self)
}
func applyIf<VM1: ViewModifier, VM2: ViewModifier>(_ condition: #autoclosure () -> Bool, ApplyIfTrue: VM1, ApplyIfFalse: VM2
) -> AnyView {
if condition() {
return self.modifier(ApplyIfTrue).erase()
} else {
return self.modifier(ApplyIfFalse).erase()
}
}
#ViewBuilder func configuresIcon() -> some View {
self.applyIf(NSProcessInfo().isOperatingSystemAtLeastVersion(NSOperatingSystemVersion(majorVersion: 14, minorVersion: 0, patchVersion: 0)),
ApplyIfTrue: NavigationConfigurationView14Modifier(),
ApplyIfFalse: modifier(NavigationConfigurationViewModifier(configure: { nv in
nv.topItem?.titleView = UIImageView(image: UIImage(named: IMAGE_NAME_HERE))
})))
}
}
If you really need #available option there are several things to try. 1) Maybe it is because of inappropriate extension point, so try to move it out of View's extension. 2)Move the logic from modifiers to the body.
For example the code below work fine.
#available(macOS 10.15, iOS 13.0, *)
struct ContentView: View {
var body: some View {
ScrollView {
if #available(macOS 11.0, iOS 14.0, *) {
LazyVStack {
ForEach(1...1000, id: \.self) { value in
Text("Row \(value)")
}
}
} else {
VStack {
ForEach(1...1000, id: \.self) { value in
Text("Row \(value)")
}
}
}
}
}
}

How to write function in SwiftUI that takes callback (function type) from user of the view so that user can chain the function calls?

I am looking to expose functions from my struct that conforms to View protocol in such a way that user of struct can chain the function calls.
Below is what I wrote
struct MyResuableView: View {
var event1Callback: () -> Void = {}
var event2Callback: () -> Void = {}
var body: some View {
Text("Resuable View")
}
mutating func onEvent1(action: #escaping () -> Void) -> Self {
self.event1Callback = action
return self
}
mutating func onEvent2(action: #escaping () -> Void) -> Self {
self.event2Callback = action
return self
}
}
This is how I am looking to use it
struct MyView: View {
var body: some View {
VStack {
Text("My View")
MyResuableView()
.onEvent1(action: self.event1Handler)
.onEvent2(action: self.event2Handler)
}
}
private func event1Handler() {
}
private func event2Handler() {
}
}
I am getting below compilation error
Cannot use mutating member on immutable value: function call returns immutable value which is understandable.
I looked at the thread SwiftUI callback as parameter to a view. This is one of the way to achieve but how to expose functions like onEvent1(....), onEvent2(....) to take functions as input from caller.
So I am just curious to know what do I need to (if we can) to achieve what I am looking for??
Here is possible approach (recalling that View is just a value)...
struct MyResuableView: View {
var event1Callback: () -> Void = {}
var event2Callback: () -> Void = {}
var body: some View {
Text("Resuable View")
}
func onEvent1(action: #escaping () -> Void) -> Self {
var newOne = self
newOne.event1Callback = action
return newOne
}
func onEvent2(action: #escaping () -> Void) -> Self {
var newOne = self
newOne.event2Callback = action
return newOne
}
}
You could structure it the way view modifiers work. Hand back a new view each time:
struct MyReusableView: View {
let event1Callback: () -> Void
let event2Callback: () -> Void
init(event1Callback: #escaping () -> Void = {}, event2Callback: #escaping () -> Void = {})
{
self.event1Callback = event1Callback
self.event2Callback = event2Callback
}
var body: some View {
Text("Reusable View")
}
func onEvent1(action: #escaping () -> Void) -> Self {
.init(event1Callback: action, event2Callback: self.event2Callback)
}
func onEvent2(action: #escaping () -> Void) -> Self {
.init(event1Callback: self.event1Callback, event2Callback: action)
}
}