How to dismiss swiftUI from Viewcontroller? - swiftui

In my an viewController I have this function
...{
let vc = UIHostingController(rootView: SwiftUIView())
present(vc, animated: true, completion: nil)
}
Which present the following SwiftUIView.
Q How to dismiss the SwiftUIView when CustomButton pressed?
struct SwiftUIView : View {
var body: some View {
CustomButton()
}
}
struct CustomButton: View {
var body: some View {
Button(action: {
self.buttonAction()
}) {
Text(buttonTitle)
}
}
func buttonAction() {
//dismiss the SwiftUIView when this button pressed
}
}

struct CustomButton: View {
var body: some View {
Button(action: {
self.buttonAction()
}) {
Text(buttonTitle)
}
}
func buttonAction() {
if let topController = UIApplication.topViewController() {
topController.dismiss(animated: true)
}
}
}
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}
Or if this doesn't work because you have a different view controller on top or if you need to use view life cycle events (onDisappear and onAppear won't work with UIHostingController).
You can use instead:
final class SwiftUIViewController: UIHostingController<CustomButton> {
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: CustomButton())
rootView.dismiss = dismiss
}
init() {
super.init(rootView: CustomButton())
rootView.dismiss = dismiss
}
func dismiss() {
dismiss(animated: true, completion: nil)
}
override func viewWillDisappear(_ animated: Bool) {
rootView.prepareExit()
}
override func viewDidDisappear(_ animated: Bool) {
rootView.doExit()
}
}
struct CustomButton: View {
var dismiss: (() -> Void)?
var body: some View {
Button(action: dismiss! ) {
Text("Dismiss")
}
}
func prepareExit() {
// code to execute on viewWillDisappear
}
func doExit() {
// code to execute on viewDidDisappear
}
}

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:

How we can adding a search bar with side bar icon to the navigation view?

I want to add a search bar to the navigation bar, but I do not know how to use search bar with sidebar icon in the same HStack. I put example screenshot with ContentView code. Any help would be appreciated.
Screenshot:
ContentView:
struct ContentView: View {
#State private var isShowing = false
var body: some View {
ZStack {
if isShowing {
SideMenuView(isShowing: $isShowing)
}
TabView {
NavigationView {
HomeView()
.navigationBarItems(leading: Button(action: {
withAnimation(.spring()) {
isShowing.toggle()
}
} , label: {
Image(systemName: "list.bullet")
}))
}
.tabItem {
Image(systemName: "1.circle")
Text("Page 1")
}
NavigationView {
HomeTwoView()
.navigationBarItems(leading: Button(action: {
withAnimation(.spring()) {
isShowing.toggle()
}
} , label: {
Image(systemName: "list.bullet")
}))
}
.tabItem {
Image(systemName: "2.circle")
Text("Page 2")
}
}
.edgesIgnoringSafeArea(.bottom)
//.cornerRadius(isShowing ? 20 : 0) //<< disabled due to strange effect
.offset(x: isShowing ? 300 : 0, y: isShowing ? 44: 0)
.scaleEffect(isShowing ? 0.8 : 1)
}.onAppear {
isShowing=false
}
}
}
As I mentioned in comments this is not possible in SwiftUI (2.0) yet. What you can do is integrating with UIKit.
Integrate with UIKit
class UIKitSearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.definesPresentationContext = true
self.searchController.searchResultsUpdater = self
}
}
extension UIKitSearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: UIKitSearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: UIKitSearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
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)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
onResolve(parent)
}
}
}
Usage
struct Example: View {
#StateObject var searchBar = UIKitSearchBar()
var body: some View {
NavigationView {
Text("Example")
.add(searchBar)
.navigationTitle("Example")
}
}
}
In my own project I am using computed property to filter stuff, it can be helpful for you too. Here is my code:
var filteredExams: [Exam] {
examModel.exams.filter({ searchBar.text.isEmpty || $0.examName.localizedStandardContains(searchBar.text)})
}
Screenshot

SwiftUI SearchBar problem with NavigationLink

I've a problem in SwiftUI with the searchBar appear.
There's a delay on its appear when I use NavigationLink. I saw that the problem appears only with NavigationLinks, if I use a conditional overlay or others "handmade" way to move between Views the problem doesn't appear. You know what I could do to fix the problem?
Here's my views code:
import SwiftUI
struct ContentView: View {
#State var searchText = ""
var body: some View {
NavigationView{
NavigationLink(destination: ContentView2()){
Text("Go to Sub View")
}
.navigationBarTitle("Main View")
.add(SearchBar(text: self.$searchText, hide: true, placeholder: "Search", cancelButton: true, autocapitalization: .sentences))
}
}
}
struct ContentView2 : View {
#State var searchText = ""
var body: some View {
Text("Hello, world!")
.navigationBarTitle("Sub View")
.add(SearchBar(text: self.$searchText, hide: true, placeholder: "Search", cancelButton: true, autocapitalization: .sentences))
}
}
My SearchBar code
import SwiftUI
class SearchBar: NSObject, ObservableObject {
let searchController: UISearchController = UISearchController(searchResultsController: nil)
#Binding var text: String
let hide : Bool
let placeholder : String
let cancelButton : Bool
let autocapitalization : UITextAutocapitalizationType
init(text: Binding<String>, hide: Bool, placeholder: String, cancelButton: Bool, autocapitalization: UITextAutocapitalizationType) {
self._text = text
self.hide = hide
self.placeholder = placeholder
self.cancelButton = cancelButton
self.autocapitalization = autocapitalization
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
self.searchController.hidesNavigationBarDuringPresentation = hide
self.searchController.automaticallyShowsCancelButton = cancelButton
self.searchController.searchBar.placeholder = placeholder
self.searchController.searchBar.autocapitalizationType = autocapitalization
}
}
extension SearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: SearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
My ViewController code
import SwiftUI
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)
}
}
override func viewDidAppear(_ animated: Bool) {
self.parent?.navigationItem.hidesSearchBarWhenScrolling = false
self.parent?.definesPresentationContext = true
self.parent?.navigationController?.navigationBar.sizeToFit()
}
override func viewDidDisappear(_ animated: Bool) {
self.parent?.navigationItem.hidesSearchBarWhenScrolling = false
self.parent?.definesPresentationContext = true
self.parent?.navigationController?.navigationBar.sizeToFit()
}
}
And here's a video of the problem
Set the hidesSearchBarWhenScrolling property before the SearchBar is displayed on the screen. This can be done in viewWillAppear or as in the example below:
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)
)
}
}

SKStoreProductViewController must be used in a modal view controller SWIFTUI

I am building a info page for my SwiftUI app. One item should open App Store, another mail. I have written UIViewControllerRepresentable for each.
MailView works fine totally. StoreView displays fine, but when pressed on Cancel button, throws exception
"*** Terminating app due to uncaught exception 'SKUnsupportedPresentationException', reason: 'SKStoreProductViewController must be used in a modal view controller'".
MailView goes fine into didFinish delegate method but StoreView does not go into didFinish delegate method, it crashes before going into this didFinish method. What am I doing wrong please?
import SwiftUI
import StoreKit
import MessageUI
struct InfoMoreAppsView: View {
#State var showAppAtStore = false
#State var reportBug = false
#State var result: Result<MFMailComposeResult, Error>? = nil
let otherAppName = "TheoryTest"
var body: some View {
VStack(alignment: .leading){
HStack{
Image(Helper.getOtherAppImageName(otherAppName: otherAppName))
Button(action: { self.showAppAtStore = true }) {
Text(otherAppName)
}
.sheet(isPresented: $showAppAtStore){
StoreView(appID: Helper.getOtherAppID(otherAppName: otherAppName))
}
}
Button(action: { self.reportBug = true }) {
Text("Report a bug")
}
.sheet(isPresented: $reportBug){
MailView(result: self.$result)
}
}
.padding()
.font(.title2)
}
}
struct StoreView: UIViewControllerRepresentable {
let appID: String
#Environment(\.presentationMode) var presentation
class Coordinator: NSObject, SKStoreProductViewControllerDelegate {
#Binding var presentation: PresentationMode
init(presentation: Binding<PresentationMode> ) {
_presentation = presentation
}
private func productViewControllerDidFinish(viewController: SKStoreProductViewController) {
$presentation.wrappedValue.dismiss()
viewController.dismiss(animated: true, completion: nil)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<StoreView>) -> SKStoreProductViewController {
let skStoreProductViewController = SKStoreProductViewController()
skStoreProductViewController.delegate = context.coordinator
let parameters = [ SKStoreProductParameterITunesItemIdentifier : appID]
skStoreProductViewController.loadProduct(withParameters: parameters)
return skStoreProductViewController
}
func updateUIViewController(_ uiViewController: SKStoreProductViewController, context: UIViewControllerRepresentableContext<StoreView>) {
}
}
struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_presentation = presentation
_result = result
}
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let mailComposeViewController = MFMailComposeViewController()
mailComposeViewController.mailComposeDelegate = context.coordinator
mailComposeViewController.setToRecipients([Constants.SUPPORT_EMAIL])
mailComposeViewController.setMessageBody(systemInfo(), isHTML: true)
return mailComposeViewController
}
func systemInfo() -> String {
let device = UIDevice.current
let systemVersion = device.systemVersion
let model = UIDevice.hardwareModel
let mailBody = "Model: " + model + ". OS: " + systemVersion
return mailBody
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
This isn't very "Swifty" or pretty but I got this to work without crashing by not wrapping the SKStoreProductViewController in a representable.
struct MovieView: View {
var vc:SKStoreProductViewController = SKStoreProductViewController()
var body: some View {
HStack(){
Button(action: {
let params = [
SKStoreProductParameterITunesItemIdentifier:"1179624268",
SKStoreProductParameterAffiliateToken:"11l4Cu",
SKStoreProductParameterCampaignToken:"hype_movie"
] as [String : Any]
// vc!.delegate = self
vc.loadProduct(withParameters: params, completionBlock: { (success,error) -> Void in
UIApplication.shared.windows.first?.rootViewController?.present(vc, animated: true, completion: nil)
})
}) {
HStack {
Image(systemName: "play.fill")
.font(.headline)
}
.padding(EdgeInsets(top: 6, leading:36, bottom: 6, trailing: 36))
.foregroundColor(.white)
.background(Color(red: 29/255, green: 231/255, blue: 130/255))
.cornerRadius(10)
}
Spacer()
}}
Since I was stuck on the same thing. Here is a quick solution I found working.
import StoreKit
import SwiftUI
import UIKit
struct StoreView: UIViewControllerRepresentable {
var dismissHandler: () -> Void
func makeUIViewController(context: UIViewControllerRepresentableContext<StoreView>) -> StoreViewController {
return StoreViewController(coordinator: context.coordinator)
}
func updateUIViewController(_ uiViewController: StoreViewController, context: UIViewControllerRepresentableContext<StoreView>) {
}
public func makeCoordinator() -> StoreViewCoordinator {
.init(dismissHandler: dismissHandler)
}
}
class StoreViewController: UIViewController {
let coordinator: StoreViewCoordinator
var storeController: SKStoreProductViewController?
init(coordinator: StoreViewCoordinator) {
self.coordinator = coordinator
super.init(nibName: nil, bundle: nil)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
storeController = SKStoreProductViewController()
storeController?.delegate = coordinator
storeController?.loadProduct(
withParameters: [SKStoreProductParameterITunesItemIdentifier: ******]
)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let storeController = storeController else {
return
}
present(storeController, animated: true)
}
}
class StoreViewCoordinator: NSObject, SKStoreProductViewControllerDelegate {
private let dismissHandler: () -> Void
init(dismissHandler: #escaping () -> Void) {
self.dismissHandler = dismissHandler
}
func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) {
dismissHandler()
}
}
and then I am using it inside ZStack like:
StoreView(
dismissHandler: { viewStore.send(.setShowingStore(false)) }
)
.isHidden(!viewStore.isShowingStore, remove: true)
I am using TCA, so setting a property will be different in your case