IGListKitSections doesn't get deallocated - swift

I have a problem with IGListKit sections deallocating. Trying to debug the issue with Xcode memory graph.
My setup is AuthController -> AuthViewModel -> AuthSocialSectionController -> AuthSocialViewModel and some other sections.
AuthController gets presented from several parts of the app if user is not logged in. When I tap close, AuthViewModel and AuthController gets deallocated, but it's underlying sections does not. Memory graph shows nothing leaked in this case, but deinit methods doesn't get called.
But when I'm trying to authorize with social account (successfully) and then look at the memory graph, it shows that sections, that doesn't get deallocated like this:
In this case AuthViewModel doesn't get deallocated either, but after some time it does, but it can happen or not.
I checked every closure and delegate for weak reference, but still no luck.
My code, that I think makes most sense:
class AuthViewController: UIViewController {
fileprivate let collectionView: UICollectionView = UICollectionView(frame: .zero,
collectionViewLayout: UICollectionViewFlowLayout())
lazy var adapter: ListAdapter
= ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)
fileprivate lazy var previewProxy: SJListPreviewProxy = {
SJListPreviewProxy(adapter: adapter)
}()
fileprivate let viewModel: AuthViewModel
fileprivate let disposeBag = DisposeBag()
init(with viewModel: AuthViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
hidesBottomBarWhenPushed = true
setupObservers()
}
private func setupObservers() {
NotificationCenter.default.rx.notification(.SJAProfileDidAutoLogin)
.subscribe(
onNext: { [weak self] _ in
self?.viewModel.didSuccessConfirmationEmail()
self?.viewModel.recoverPasswordSuccess()
})
.disposed(by: disposeBag)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - View Controller Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
// MARK: - Private
#objc private func close() {
dismiss(animated: true, completion: nil)
}
/// Метод настройки экрана
private func setup() {
if isForceTouchEnabled() {
registerForPreviewing(with: previewProxy, sourceView: collectionView)
}
view.backgroundColor = AppColor.instance.gray
title = viewModel.screenName
let item = UIBarButtonItem(image: #imageLiteral(resourceName: "close.pdf"), style: .plain, target: self, action: #selector(AuthViewController.close))
item.accessibilityIdentifier = "auth_close_btn"
asViewController.navigationItem.leftBarButtonItem = item
navigationItem.titleView = UIImageView(image: #imageLiteral(resourceName: "logo_superjob.pdf"))
collectionViewSetup()
}
// Настройка collectionView
private func collectionViewSetup() {
collectionView.keyboardDismissMode = .onDrag
collectionView.backgroundColor = AppColor.instance.gray
view.addSubview(collectionView)
adapter.collectionView = collectionView
adapter.dataSource = self
collectionView.snp.remakeConstraints { make in
make.edges.equalToSuperview()
}
}
}
// MARK: - DataSource CollectionView
extension AuthViewController: ListAdapterDataSource {
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
return viewModel.sections(for: listAdapter)
}
func listAdapter(_: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
return viewModel.createListSectionController(for: object)
}
func emptyView(for _: ListAdapter) -> UIView? {
return nil
}
}
// MARK: - AuthViewModelDelegate
extension AuthViewController: AuthViewModelDelegate {
func hideAuth(authSuccessBlock: AuthSuccessAction?) {
dismiss(animated: true, completion: {
authSuccessBlock?()
})
}
func reload(animated: Bool, completion: ((Bool) -> Void)? = nil) {
adapter.performUpdates(animated: animated, completion: completion)
}
func showErrorPopover(with item: CommonAlertPopoverController.Item,
and anchors: (sourceView: UIView, sourceRect: CGRect)) {
let popover = CommonAlertPopoverController(with: item,
preferredContentWidth: view.size.width - 32.0,
sourceView: anchors.sourceView,
sourceRect: anchors.sourceRect,
arrowDirection: .up)
present(popover, animated: true, completion: nil)
}
}
class AuthViewModel {
fileprivate let assembler: AuthSectionsAssembler
fileprivate let router: AuthRouter
fileprivate let profileFacade: SJAProfileFacade
fileprivate let api3ProfileFacade: API3ProfileFacade
fileprivate let analytics: AnalyticsProtocol
fileprivate var sections: [Section] = []
weak var authDelegate: AuthDelegate?
weak var vmDelegate: AuthViewModelDelegate?
var authSuccessBlock: AuthSuccessAction?
private lazy var socialSection: AuthSocialSectionViewModel = { [unowned self] in
self.assembler.socialSection(delegate: self)
}()
init(assembler: AuthSectionsAssembler,
router: AuthRouter,
profileFacade: SJAProfileFacade,
api3ProfileFacade: API3ProfileFacade,
analytics: AnalyticsProtocol,
delegate: AuthDelegate? = nil,
purpose: Purpose) {
self.purpose = purpose
authDelegate = delegate
self.assembler = assembler
self.router = router
self.profileFacade = profileFacade
self.api3ProfileFacade = api3ProfileFacade
self.analytics = analytics
sections = displaySections()
}
private func authDisplaySections() -> [Section] {
let sections: [Section?] = [vacancySection,
authHeaderSection,
socialSection,
authLoginPasswordSection,
signInButtonSection,
switchToSignUpButtonSection,
recoverPasswordSection]
return sections.compactMap { $0 }
}
}
class AuthSocialSectionController: SJListSectionController, SJUpdateCellsLayoutProtocol {
fileprivate let viewModel: AuthSocialSectionViewModel
init(viewModel: AuthSocialSectionViewModel) {
self.viewModel = viewModel
super.init()
minimumInteritemSpacing = 4
viewModel.vmDelegate = self
}
override func cellType(at _: Int) -> UICollectionViewCell.Type {
return AuthSocialCell.self
}
override func cellInitializationType(at _: Int) -> SJListSectionCellInitializationType {
return .code
}
override func configureCell(_ cell: UICollectionViewCell, at index: Int) {
guard let itemCell = cell as? AuthSocialCell else {
return
}
let item = viewModel.item(at: index)
itemCell.imageView.image = item.image
}
override func separationStyle(at _: Int) -> SJCollectionViewCellSeparatorStyle {
return .none
}
}
extension AuthSocialSectionController {
override func numberOfItems() -> Int {
return viewModel.numberOfItems
}
override func didSelectItem(at index: Int) {
viewModel.didSelectItem(at: index)
}
}
// MARK: - AuthSocialSectionViewModelDelegate
extension AuthSocialSectionController: AuthSocialSectionViewModelDelegate {
func sourceViewController() -> UIViewController {
return viewController ?? UIViewController()
}
}
protocol AuthSocialSectionDelegate: class {
func successfullyAuthorized(type: SJASocialAuthorizationType)
func showError(with error: Error)
}
protocol AuthSocialSectionViewModelDelegate: SJListSectionControllerOperationsProtocol, ViewControllerProtocol {
func sourceViewController() -> UIViewController
}
class AuthSocialSectionViewModel: NSObject {
struct Item {
let image: UIImage
let type: SJASocialAuthorizationType
}
weak var delegate: AuthSocialSectionDelegate?
weak var vmDelegate: AuthSocialSectionViewModelDelegate?
fileprivate var items: [Item]
fileprivate let api3ProfileFacade: API3ProfileFacade
fileprivate let analyticsFacade: SJAAnalyticsFacade
fileprivate var socialButtonsDisposeBag = DisposeBag()
init(api3ProfileFacade: API3ProfileFacade,
analyticsFacade: SJAAnalyticsFacade) {
self.api3ProfileFacade = api3ProfileFacade
self.analyticsFacade = analyticsFacade
items = [
Item(image: #imageLiteral(resourceName: "ok_icon.pdf"), type: .OK),
Item(image: #imageLiteral(resourceName: "vk_icon.pdf"), type: .VK),
Item(image: #imageLiteral(resourceName: "facebook_icon.pdf"), type: .facebook),
Item(image: #imageLiteral(resourceName: "mail_icon.pdf"), type: .mail),
Item(image: #imageLiteral(resourceName: "google_icon.pdf"), type: .google),
Item(image: #imageLiteral(resourceName: "yandex_icon.pdf"), type: .yandex)
]
if analyticsFacade.isHHAuthAvailable() {
items.append(Item(image: #imageLiteral(resourceName: "hh_icon"), type: .HH))
}
}
// MARK: - actions
func didSelectItem(at index: Int) {
guard let vc = vmDelegate?.sourceViewController() else {
return
}
let itemType: SJASocialAuthorizationType = items[index].type
socialButtonsDisposeBag = DisposeBag()
api3ProfileFacade.authorize(with: itemType, sourceViewController: vc)
.subscribe(
onNext: { [weak self] _ in
self?.delegate?.successfullyAuthorized(type: itemType)
},
onError: { [weak self] error in
if case let .detailed(errorModel)? = error as? ApplicantError {
self?.vmDelegate?.asViewController.showError(with: errorModel.errors.first?.detail ?? "")
} else {
self?.vmDelegate?.asViewController.showError(with: "Неизвестная ошибка")
}
})
.disposed(by: socialButtonsDisposeBag)
}
}
// MARK: - DataSource
extension AuthSocialSectionViewModel {
var numberOfItems: Int {
return items.count
}
func item(at index: Int) -> Item {
return items[index]
}
}
// MARK: - ListDiffable
extension AuthSocialSectionViewModel: ListDiffable {
func diffIdentifier() -> NSObjectProtocol {
return ObjectIdentifier(self).hashValue as NSObjectProtocol
}
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
return object is AuthSocialSectionViewModel
}
}
Where assembler is responsible for creating everyting, for example AuthSocialSection:
func socialSection(delegate: AuthSocialSectionDelegate?) -> AuthSocialSectionViewModel {
let vm = AuthSocialSectionViewModel(api3ProfileFacade: api3ProfileFacade,
analyticsFacade: analyticsFacade)
vm.delegate = delegate
return vm
}
How can I properly debug this issue? Any advice or help is really appreciated

Found an issue in AuthSocialSectionController. Somehow passing viewController from IGList context through delegates caused memory issues. When I commented out the viewModel.vmDelegate = self the issue was gone.
That explains why the AuthViewModel was deallocating properly when I hit close button without attempting to authorize. Only when I hit authorize, that viewController property was called.
Thanks for help #vpoltave

This lines from your AuthViewController can this cause leaks?
// adapter has viewController: self
lazy var adapter: ListAdapter
= ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)
fileprivate lazy var previewProxy: SJListPreviewProxy = {
// capture self.adapter ?
SJListPreviewProxy(adapter: adapter)
}()
I'm not sure, but at least you can try :)
UPDATE
I was wondering about this lazy closures and self inside, it won't create retain cycle because lazy initialization are #nonescaping.

Related

Using UIEditMenuInteraction with UITextView

How can we use UIEditMenuInteraction with UITextView to customize menu and add more buttons?
Before iOS 16 I was using:
UIMenuController.shared.menuItems = [menuItem1, menuItem2, menuItem3]
Try this sample source:
class ViewController: UIViewController {
#IBOutlet weak var txtView: UITextView!
var editMenuInteraction: UIEditMenuInteraction?
override func viewDidLoad() {
super.viewDidLoad()
setupEditMenuInteraction()
}
private func setupEditMenuInteraction() {
// Addding Menu Interaction to TextView
editMenuInteraction = UIEditMenuInteraction(delegate: self)
txtView.addInteraction(editMenuInteraction!)
// Addding Long Press Gesture
let longPressGestureRecognizer =
UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
txtView.addGestureRecognizer(longPressGestureRecognizer)
}
#objc
func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
guard gestureRecognizer.state == .began else { return }
let configuration = UIEditMenuConfiguration(
identifier: "textViewEdit",
sourcePoint: gestureRecognizer.location(in: txtView)
)
editMenuInteraction?.presentEditMenu(with: configuration)
}
}
extension ViewController: UIEditMenuInteractionDelegate {
func editMenuInteraction(_ interaction: UIEditMenuInteraction,
menuFor configuration: UIEditMenuConfiguration,
suggestedActions: [UIMenuElement]) -> UIMenu? {
var actions = suggestedActions
let customMenu = UIMenu(title: "", options: .displayInline, children: [
UIAction(title: "menuItem1") { _ in
print("menuItem1")
},
UIAction(title: "menuItem2") { _ in
print("menuItem2")
},
UIAction(title: "menuItem3") { _ in
print("menuItem3")
}
])
actions.append(customMenu)
return UIMenu(children: actions) // For Custom and Suggested Menu
return UIMenu(children: customMenu.children) // For Custom Menu Only
}
}
Output

How to import or access UITextField from another Class

I have Outlet of UITextField in the main VC and I want to access its text from another class... for more clarification please see my code below:
class ProductManagement : UIViewController, UITextFieldDelegate{
#IBOutlet weak var ProductName: UITextField!
}
and I want to read the text in below class
import Firebase
import FirebaseStorage
class XUpload {
static func UploadImage(Image : UIImage, Completion : #escaping (_ url : String)->()) {
guard let imageData = Image.pngData() else { return }
let storage = Storage.storage().reference()
let pathRef = storage.child("Images/Products")
let imageRef = pathRef.child( // I want the product name to be written here! )
imageRef.putData(imageData, metadata: nil) { (meta, error) in
imageRef.downloadURL { (url, error) in
print (url as Any)
}
}
}
}
I tried to create a custom protocol to delegate the UITextField but the problem is I couldn't conform it inside XUpload class !!
Please someone write for me how is my code should be because I'm beginner and new to Swift language.
Thank you in advance.
EDIT:
Elia Answer applied below:
ProductManagement Class + Extension
class ProductManagement : UIViewController, UITextFieldDelegate{
override func viewDidLoad() {
super.viewDidLoad()
self.ProductName.delegate = self
}
#IBOutlet weak var ProductName: UITextField!
#objc private func textFieldDidChange(_ sender: UITextField) {
NotificationCenter.default.post(name: .PnameInputText, object: nil, userInfo: ["text": sender.text!])
}
}
extension Notification.Name {
public static var PnameInputText = Notification.Name("PnameInputText")
}
XUpload Class
import Firebase
import FirebaseStorage
class XUpload {
private init() {
NotificationCenter.default.addObserver(self, selector: #selector(handle(_:)), name: .PnameInputText, object: nil)
}
public static var shared = XUpload()
var textFieldText : String = "" {
didSet {print("updated value > ", textFieldText)}
}
#objc private func handle(_ sender: Notification) {
if let userInfo = sender.userInfo as NSDictionary?, let text = userInfo["text"] as? String {
textFieldText = text
}
}
static func UploadImage(Image : UIImage, Completion : #escaping (_ url : String)->()) {
guard let imageData = Image.pngData() else { return }
let storage = Storage.storage().reference()
let pathRef = storage.child("Images/Products")
let imageRef = pathRef.child("img_"+shared.textFieldText)
print("var of textFieldText value: " + shared.textFieldText) //print nothing... empty value!!
imageRef.putData(imageData, metadata: nil) { (meta, error) in
imageRef.downloadURL { (url, error) in
print ("Image uploaded to this link >> " + url!.description)
guard let str = url?.absoluteString else { return }
Completion (str)
}
}
}
}
extension UIImage {
func upload(completion : #escaping (_ url : String) ->()) {
XUpload.UploadImage(Image: self) { (ImageURL) in completion (ImageURL)
}
}
}
As you can see there is no value given to var textFieldText by the Notification Center! why??
class ProductManagement : UIViewController, UITextFieldDelegate{
var productInputText: String = ""
#IBOutlet weak var ProductName: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
ProductName.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}
#objc private func textFieldDidChange(_ sender: UITextField) {
productInputText = sender.text ?? ""
}
}
This will store your input text into a variable basically with listen edit changes.
And if I get not wrong, you use UploadImage within ProductManagement class. Add a function property to UploadImage as below
class XUpload {
static func UploadImage(Image : UIImage, productName: String, Completion : #escaping (_ url : String)->()) {
guard let imageData = Image.pngData() else { return }
let storage = Storage.storage().reference()
let pathRef = storage.child("Images/Products")
let imageRef = pathRef.child( // I want the product name to be written here! )
imageRef.putData(imageData, metadata: nil) { (meta, error) in
imageRef.downloadURL { (url, error) in
print (url as Any)
}
}
}
}
and call it in ProductManagement class like below.
XUpload.UploadImage(image: myImage, productName: productInputText, Completion: {
}
EDIT:
After getting your comment, I decided the best way do it is using Notification, you use UITextField in ProductManagement class so there is no need to handle delegate method of them in XUpload
Describe notification name
extension Notification.Name {
public static var TextChange = Notification.Name("TextChange")
}
In ProductManagement, textDidChange method post text to Notification
#objc private func textFieldDidChange(_ sender: UITextField) {
NotificationCenter.default.post(name: .TextChange, object: nil, userInfo: ["text": sender.text])
}
I convert your XUpload class to a Singleton class.
class XUpload {
private init() {
NotificationCenter.default.addObserver(self, selector: #selector(handle(_:)), name: .TextChange, object: nil)
}
public static var Shared = XUpload()
#objc private func handle(_ sender: Notification) {
if let userInfo = sender.userInfo as NSDictionary?, let text = userInfo["text"] as? String {
textFieldText = text
}
}
var textFieldText: String = "" {
didSet {
print("updated value > " , textFieldText)
}
}
static func uploadImage() {
// use updated textfield text with textFieldText
}
}
Then store a variable in ProductManagement class as a singleton object and it wil work for you. The text in textfield updated every changes in XUpload class.
class ProductManagement : UIViewController, UITextFieldDelegate{
var staticVariable = XUpload.Shared
}

SwiftUI sheet() modals with custom size on iPad

How can I control the preferred presentation size of a modal sheet on iPad with SwiftUI? I'm surprised how hard it is to find an answer on Google for this.
Also, what is the best way to know if the modal is dismissed by dragging it down (cancelled) or actually performing a custom positive action?
Here is my solution for showing a form sheet on an iPad in SwiftUI:
struct MyView: View {
#State var show = false
var body: some View {
Button("Open Sheet") { self.show = true }
.formSheet(isPresented: $show) {
Text("Form Sheet Content")
}
}
}
Enabled by this UIViewControllerRepresentable
class FormSheetWrapper<Content: View>: UIViewController, UIPopoverPresentationControllerDelegate {
var content: () -> Content
var onDismiss: (() -> Void)?
private var hostVC: UIHostingController<Content>?
required init?(coder: NSCoder) { fatalError("") }
init(content: #escaping () -> Content) {
self.content = content
super.init(nibName: nil, bundle: nil)
}
func show() {
guard hostVC == nil else { return }
let vc = UIHostingController(rootView: content())
vc.view.sizeToFit()
vc.preferredContentSize = vc.view.bounds.size
vc.modalPresentationStyle = .formSheet
vc.presentationController?.delegate = self
hostVC = vc
self.present(vc, animated: true, completion: nil)
}
func hide() {
guard let vc = self.hostVC, !vc.isBeingDismissed else { return }
dismiss(animated: true, completion: nil)
hostVC = nil
}
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
hostVC = nil
self.onDismiss?()
}
}
struct FormSheet<Content: View> : UIViewControllerRepresentable {
#Binding var show: Bool
let content: () -> Content
func makeUIViewController(context: UIViewControllerRepresentableContext<FormSheet<Content>>) -> FormSheetWrapper<Content> {
let vc = FormSheetWrapper(content: content)
vc.onDismiss = { self.show = false }
return vc
}
func updateUIViewController(_ uiViewController: FormSheetWrapper<Content>,
context: UIViewControllerRepresentableContext<FormSheet<Content>>) {
if show {
uiViewController.show()
}
else {
uiViewController.hide()
}
}
}
extension View {
public func formSheet<Content: View>(isPresented: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content) -> some View {
self.background(FormSheet(show: isPresented,
content: content))
}
}
You should be able to modify the code in func show() according to UIKit specs in order to get the sizing the way you like (and you can even go so far as to inject parameters from the SwiftUI side if needed). This is just how I get a form sheet to work on iPad as .sheet was just too big for my use case
I just posted the same in this SO How can I make a background color with opacity on a Sheet view?
but it seems to do exactly what I need it to do. It makes the background of the sheet transparent while allowing the content to be sized as needed to appear as if it's the only part of the sheet. Works great on the iPad.
Using the AWESOME answer from #Asperi that I have been trying to find all day, I have built a simple view modifier that can now be applied inside a .sheet or .fullScreenCover modal view and provides a transparent background. You can then set the frame modifier for the content as needed to fit the screen without the user having to know the modal is not custom sized.
import SwiftUI
struct ClearBackgroundView: UIViewRepresentable {
func makeUIView(context: Context) -> some UIView {
let view = UIView()
DispatchQueue.main.async {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
struct ClearBackgroundViewModifier: ViewModifier {
func body(content: Content) -> some View {
content
.background(ClearBackgroundView())
}
}
extension View {
func clearModalBackground()->some View {
self.modifier(ClearBackgroundViewModifier())
}
}
Usage:
.sheet(isPresented: $isPresented) {
ContentToDisplay()
.frame(width: 300, height: 400)
.clearModalBackground()
}
In case it helps anyone else, I was able to get this working by leaning on this code to hold the view controller:
https://gist.github.com/timothycosta/a43dfe25f1d8a37c71341a1ebaf82213
struct ViewControllerHolder {
weak var value: UIViewController?
init(_ value: UIViewController?) {
self.value = value
}
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder? { ViewControllerHolder(UIApplication.shared.windows.first?.rootViewController) }
}
extension EnvironmentValues {
var viewController: ViewControllerHolder? {
get { self[ViewControllerKey.self] }
set { self[ViewControllerKey.self] = newValue }
}
}
extension UIViewController {
func present<Content: View>(
presentationStyle: UIModalPresentationStyle = .automatic,
transitionStyle _: UIModalTransitionStyle = .coverVertical,
animated: Bool = true,
completion: #escaping () -> Void = { /* nothing by default*/ },
#ViewBuilder builder: () -> Content
) {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = presentationStyle
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, ViewControllerHolder(toPresent))
)
if presentationStyle == .overCurrentContext {
toPresent.view.backgroundColor = .clear
}
present(toPresent, animated: animated, completion: completion)
}
}
Coupled with a specialized view to handle common elements in the modal:
struct ModalContentView<Content>: View where Content: View {
// Use this function to provide the content to display and to bring up the modal.
// Currently only the 'formSheet' style has been tested but it should work with any
// modal presentation style from UIKit.
public static func present(_ content: Content, style: UIModalPresentationStyle = .formSheet) {
let modal = ModalContentView(content: content)
// Present ourselves
modal.viewController?.present(presentationStyle: style) {
modal.body
}
}
// Grab the view controller out of the environment.
#Environment(\.viewController) private var viewControllerHolder: ViewControllerHolder?
private var viewController: UIViewController? {
viewControllerHolder?.value
}
// The content to be displayed in the view.
private var content: Content
public var body: some View {
VStack {
/// Some specialized controls, like X button to close omitted...
self.content
}
}
Finally, simply call:
ModalContentView.present( MyAwesomeView() )
to display MyAwesomeView inside of a .formSheet modal.
There are some issues in #ccwasden's answer. Dismissing popover won't change $isPresented all the time, as delegate is not set and hostVC is never assigned.
Here are some modifications required.
In FlexSheetWrapper:
func show() {
guard hostVC == nil else { return }
let vc = UIHostingController(rootView: content())
vc.view.sizeToFit()
vc.preferredContentSize = vc.view.bounds.size
vc.modalPresentationStyle = .formSheet
vc.presentationController?.delegate = self
hostVC = vc
self.present(vc, animated: true, completion: nil)
}
And in FormSheet:
func updateUIViewController(_ uiViewController: FlexSheetWrapper<Content>,
context: UIViewControllerRepresentableContext<FlexSheet<Content>>) {
if show {
uiViewController.show()
}
else {
uiViewController.hide()
}
}
From #ccwasden answer, I fixed the problem when you $isPresented = true at the beginning of code, the modal will not present when the view is loaded, To do so here is code View+FormSheet.swift
Result
// You can now set `test = true` at first
.formSheet(isPresented: $test) {
Text("Hi")
}
View+FormSheet.swift
import SwiftUI
class ModalUIHostingController<Content>: UIHostingController<Content>, UIPopoverPresentationControllerDelegate where Content : View {
var onDismiss: (() -> Void)
required init?(coder: NSCoder) { fatalError("") }
init(onDismiss: #escaping () -> Void, rootView: Content) {
self.onDismiss = onDismiss
super.init(rootView: rootView)
view.sizeToFit()
preferredContentSize = view.bounds.size
modalPresentationStyle = .formSheet
presentationController?.delegate = self
}
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
print("modal dismiss")
onDismiss()
}
}
class ModalUIViewController<Content: View>: UIViewController {
var isPresented: Bool
var content: () -> Content
var onDismiss: (() -> Void)
private var hostVC: ModalUIHostingController<Content>
private var isViewDidAppear = false
required init?(coder: NSCoder) { fatalError("") }
init(isPresented: Bool = false, onDismiss: #escaping () -> Void, content: #escaping () -> Content) {
self.isPresented = isPresented
self.onDismiss = onDismiss
self.content = content
self.hostVC = ModalUIHostingController(onDismiss: onDismiss, rootView: content())
super.init(nibName: nil, bundle: nil)
}
func show() {
guard isViewDidAppear else { return }
self.hostVC = ModalUIHostingController(onDismiss: onDismiss, rootView: content())
present(hostVC, animated: true)
}
func hide() {
guard !hostVC.isBeingDismissed else { return }
dismiss(animated: true)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
isViewDidAppear = true
if isPresented {
show()
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
isViewDidAppear = false
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
show()
}
}
struct FormSheet<Content: View> : UIViewControllerRepresentable {
#Binding var show: Bool
let content: () -> Content
func makeUIViewController(context: UIViewControllerRepresentableContext<FormSheet<Content>>) -> ModalUIViewController<Content> {
let onDismiss = {
self.show = false
}
let vc = ModalUIViewController(isPresented: show, onDismiss: onDismiss, content: content)
return vc
}
func updateUIViewController(_ uiViewController: ModalUIViewController<Content>,
context: UIViewControllerRepresentableContext<FormSheet<Content>>) {
if show {
uiViewController.show()
}
else {
uiViewController.hide()
}
}
}
extension View {
public func formSheet<Content: View>(isPresented: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content) -> some View {
self.background(FormSheet(show: isPresented,
content: content))
}
}

How to integrate UISearchController with SwiftUI

I have a SearchController that conforms to UIViewControllerRepresentable, and I've implemented the required protocol methods. But when I created an instance of the SearchController in a SwiftUI struct, the SearchController doesn't appear on the screen once it's loaded. Does anyone have any suggestions on how I could integrate a UISearchController with SwiftUI? Thank you!
Here's the SearchController Struct that conforms to UIViewControllerRepresentable:
struct SearchController: UIViewControllerRepresentable {
let placeholder: String
#Binding var text: String
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIViewController(context: Context) -> UISearchController {
let controller = UISearchController(searchResultsController: nil)
controller.searchResultsUpdater = context.coordinator
controller.obscuresBackgroundDuringPresentation = false
controller.hidesNavigationBarDuringPresentation = false
controller.searchBar.delegate = context.coordinator
controller.searchBar.placeholder = placeholder
return controller
}
func updateUIViewController(_ uiViewController: UISearchController, context: Context) {
uiViewController.searchBar.text = text
}
class Coordinator: NSObject, UISearchResultsUpdating, UISearchBarDelegate {
var controller: SearchController
init(_ controller: SearchController) {
self.controller = controller
}
func updateSearchResults(for searchController: UISearchController) {
let searchBar = searchController.searchBar
controller.text = searchBar.text!
}
}
}
Here is the code for my SwiftUIView that should display the SearchController:
struct SearchCarsView: View {
let cars = cardsData
// MARK: #State Properties
#State private var searchCarsText: String = ""
// MARK: Views
var body: some View {
SearchController(placeholder: "Name, Model, Year", text: $searchCarsText)
.background(Color.blue)
}
}
Here's an image of the SearchController not appearing on the screen:
(EDIT) iOS 15:
iOS 15 added the new property .searchable(). You should probably use that instead.
Original:
If anyone is still looking, I made a package to deal with this.
I'm also including the full relevant source code here for those who dislike links or just want to copy/paste.
Extension:
// Copyright © 2020 thislooksfun
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the “Software”), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import SwiftUI
import Combine
public extension View {
public func navigationBarSearch(_ searchText: Binding<String>) -> some View {
return overlay(SearchBar(text: searchText).frame(width: 0, height: 0))
}
}
fileprivate struct SearchBar: UIViewControllerRepresentable {
#Binding
var text: String
init(text: Binding<String>) {
self._text = text
}
func makeUIViewController(context: Context) -> SearchBarWrapperController {
return SearchBarWrapperController()
}
func updateUIViewController(_ controller: SearchBarWrapperController, context: Context) {
controller.searchController = context.coordinator.searchController
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
class Coordinator: NSObject, UISearchResultsUpdating {
#Binding
var text: String
let searchController: UISearchController
private var subscription: AnyCancellable?
init(text: Binding<String>) {
self._text = text
self.searchController = UISearchController(searchResultsController: nil)
super.init()
searchController.searchResultsUpdater = self
searchController.hidesNavigationBarDuringPresentation = true
searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchBar.text = self.text
self.subscription = self.text.publisher.sink { _ in
self.searchController.searchBar.text = self.text
}
}
deinit {
self.subscription?.cancel()
}
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else { return }
self.text = text
}
}
class SearchBarWrapperController: UIViewController {
var searchController: UISearchController? {
didSet {
self.parent?.navigationItem.searchController = searchController
}
}
override func viewWillAppear(_ animated: Bool) {
self.parent?.navigationItem.searchController = searchController
}
override func viewDidAppear(_ animated: Bool) {
self.parent?.navigationItem.searchController = searchController
}
}
}
Usage:
import SwiftlySearch
struct MRE: View {
let items: [String]
#State
var searchText = ""
var body: some View {
NavigationView {
List(items.filter { $0.localizedStandardContains(searchText) }) { item in
Text(item)
}.navigationBarSearch(self.$searchText)
}
}
}
You can use UISearchController directly once you resolved a reference to the underlying UIKit.UINavigationItem of the actual SwiftUI View. I made a writeup about the entire process with sample project at SwiftUI Search Bar in the Navigation Bar, but let me give you a quick overview below.
You can actually resolve the underlying UIViewController of any SwiftUI View leveraging on the fact that SwiftUI preserves the hierarchy of the view controllers (via children and parent references). Once you have the UIViewController, you can set a UISearchController to navigationItem.searchController. If you extract the search controller instance to a distinct ObservableObject, you can hook up the updateSearchResults(for:) delegate method to a #Published String property, so you can make bindings on the SwiftUI side using #ObservedObject.
Actually visual element in this case is UISearchBar, so simplest start point should be as follows
import SwiftUI
import UIKit
struct SearchView: UIViewRepresentable {
let controller = UISearchController()
func makeUIView(context: UIViewRepresentableContext<SearchView>) -> UISearchBar {
self.controller.searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchView>) {
}
typealias UIViewType = UISearchBar
}
struct TestSearchController: View {
var body: some View {
SearchView()
}
}
struct TestSearchController_Previews: PreviewProvider {
static var previews: some View {
TestSearchController()
}
}
everything next can be configured in init and coordinator made as delegate, as usual.
UISearchController works when attached to a List. It becomes buggy when you use something other like a ScrollView or VStack. I have reported this in Apple's Feedback app and I hope others will do the same.
Never the less if you like to include a UISearchController in your App. I created a Swift Package at Github named NavigationSearchBar.
Code
// Copyright © 2020 Mark van Wijnen
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the “Software”), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import SwiftUI
public extension View {
func navigationSearchBar(text: Binding<String>, scopeSelection: Binding<Int> = Binding.constant(0), options: [NavigationSearchBarOptionKey : Any] = [NavigationSearchBarOptionKey : Any](), actions: [NavigationSearchBarActionKey : NavigationSearchBarActionTask] = [NavigationSearchBarActionKey : NavigationSearchBarActionTask]()) -> some View {
overlay(NavigationSearchBar<AnyView>(text: text, scopeSelection: scopeSelection, options: options, actions: actions).frame(width: 0, height: 0))
}
func navigationSearchBar<SearchResultsContent>(text: Binding<String>, scopeSelection: Binding<Int> = Binding.constant(0), options: [NavigationSearchBarOptionKey : Any] = [NavigationSearchBarOptionKey : Any](), actions: [NavigationSearchBarActionKey : NavigationSearchBarActionTask] = [NavigationSearchBarActionKey : NavigationSearchBarActionTask](), #ViewBuilder searchResultsContent: #escaping () -> SearchResultsContent) -> some View where SearchResultsContent : View {
overlay(NavigationSearchBar<SearchResultsContent>(text: text, scopeSelection: scopeSelection, options: options, actions: actions, searchResultsContent: searchResultsContent).frame(width: 0, height: 0))
}
}
public struct NavigationSearchBarOptionKey: Hashable, Equatable, RawRepresentable {
public static let automaticallyShowsSearchBar = NavigationSearchBarOptionKey("automaticallyShowsSearchBar")
public static let obscuresBackgroundDuringPresentation = NavigationSearchBarOptionKey("obscuresBackgroundDuringPresentation")
public static let hidesNavigationBarDuringPresentation = NavigationSearchBarOptionKey("hidesNavigationBarDuringPresentation")
public static let hidesSearchBarWhenScrolling = NavigationSearchBarOptionKey("hidesSearchBarWhenScrolling")
public static let placeholder = NavigationSearchBarOptionKey("Placeholder")
public static let showsBookmarkButton = NavigationSearchBarOptionKey("showsBookmarkButton")
public static let scopeButtonTitles = NavigationSearchBarOptionKey("scopeButtonTitles")
public static func == (lhs: NavigationSearchBarOptionKey, rhs: NavigationSearchBarOptionKey) -> Bool {
return lhs.rawValue == rhs.rawValue
}
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
public init(_ rawValue: String) {
self.init(rawValue: rawValue)
}
}
public struct NavigationSearchBarActionKey: Hashable, Equatable, RawRepresentable {
public static let onCancelButtonClicked = NavigationSearchBarActionKey("onCancelButtonClicked")
public static let onSearchButtonClicked = NavigationSearchBarActionKey("onSearchButtonClicked")
public static let onBookmarkButtonClicked = NavigationSearchBarActionKey("onBookmarkButtonClicked")
public static func == (lhs: NavigationSearchBarActionKey, rhs: NavigationSearchBarActionKey) -> Bool {
return lhs.rawValue == rhs.rawValue
}
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
public init(_ rawValue: String) {
self.init(rawValue: rawValue)
}
}
public typealias NavigationSearchBarActionTask = () -> Void
fileprivate struct NavigationSearchBar<SearchResultsContent>: UIViewControllerRepresentable where SearchResultsContent : View {
typealias UIViewControllerType = Wrapper
typealias OptionKey = NavigationSearchBarOptionKey
typealias ActionKey = NavigationSearchBarActionKey
typealias ActionTask = NavigationSearchBarActionTask
#Binding var text: String
#Binding var scopeSelection: Int
let options: [OptionKey : Any]
let actions: [ActionKey : ActionTask]
let searchResultsContent: () -> SearchResultsContent?
init(text: Binding<String>, scopeSelection: Binding<Int> = Binding.constant(0), options: [OptionKey : Any] = [OptionKey : Any](), actions: [ActionKey : ActionTask] = [ActionKey : ActionTask](), #ViewBuilder searchResultsContent: #escaping () -> SearchResultsContent? = { nil }) {
self._text = text
self._scopeSelection = scopeSelection
self.options = options
self.actions = actions
self.searchResultsContent = searchResultsContent
}
func makeCoordinator() -> Coordinator {
Coordinator(representable: self)
}
func makeUIViewController(context: Context) -> Wrapper {
Wrapper()
}
func updateUIViewController(_ wrapper: Wrapper, context: Context) {
if wrapper.searchController != context.coordinator.searchController {
wrapper.searchController = context.coordinator.searchController
}
if let hidesSearchBarWhenScrolling = options[.hidesSearchBarWhenScrolling] as? Bool {
wrapper.hidesSearchBarWhenScrolling = hidesSearchBarWhenScrolling
}
if options[.automaticallyShowsSearchBar] as? Bool == nil || options[.automaticallyShowsSearchBar] as! Bool {
wrapper.navigationBarSizeToFit()
}
if let searchController = wrapper.searchController {
searchController.automaticallyShowsScopeBar = true
if let obscuresBackgroundDuringPresentation = options[.obscuresBackgroundDuringPresentation] as? Bool {
searchController.obscuresBackgroundDuringPresentation = obscuresBackgroundDuringPresentation
} else {
searchController.obscuresBackgroundDuringPresentation = false
}
if let hidesNavigationBarDuringPresentation = options[.hidesNavigationBarDuringPresentation] as? Bool {
searchController.hidesNavigationBarDuringPresentation = hidesNavigationBarDuringPresentation
}
if let searchResultsContent = searchResultsContent() {
(searchController.searchResultsController as? UIHostingController<SearchResultsContent>)?.rootView = searchResultsContent
}
}
if let searchBar = wrapper.searchController?.searchBar {
searchBar.text = text
if let placeholder = options[.placeholder] as? String {
searchBar.placeholder = placeholder
}
if let showsBookmarkButton = options[.showsBookmarkButton] as? Bool {
searchBar.showsBookmarkButton = showsBookmarkButton
}
if let scopeButtonTitles = options[.scopeButtonTitles] as? [String] {
searchBar.scopeButtonTitles = scopeButtonTitles
}
searchBar.selectedScopeButtonIndex = scopeSelection
}
}
class Coordinator: NSObject, UISearchControllerDelegate, UISearchResultsUpdating, UISearchBarDelegate {
let representable: NavigationSearchBar
let searchController: UISearchController
init(representable: NavigationSearchBar) {
self.representable = representable
var searchResultsController: UIViewController? = nil
if let searchResultsContent = representable.searchResultsContent() {
searchResultsController = UIHostingController<SearchResultsContent>(rootView: searchResultsContent)
}
self.searchController = UISearchController(searchResultsController: searchResultsController)
super.init()
self.searchController.searchResultsUpdater = self
self.searchController.searchBar.delegate = self
}
// MARK: - UISearchResultsUpdating
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else { return }
DispatchQueue.main.async { [weak self] in self?.representable.text = text }
}
// MARK: - UISearchBarDelegate
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
guard let action = self.representable.actions[.onCancelButtonClicked] else { return }
DispatchQueue.main.async { action() }
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let action = self.representable.actions[.onSearchButtonClicked] else { return }
DispatchQueue.main.async { action() }
}
func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) {
guard let action = self.representable.actions[.onBookmarkButtonClicked] else { return }
DispatchQueue.main.async { action() }
}
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
DispatchQueue.main.async { [weak self] in self?.representable.scopeSelection = selectedScope }
}
}
class Wrapper: UIViewController {
var searchController: UISearchController? {
get {
self.parent?.navigationItem.searchController
}
set {
self.parent?.navigationItem.searchController = newValue
}
}
var hidesSearchBarWhenScrolling: Bool {
get {
self.parent?.navigationItem.hidesSearchBarWhenScrolling ?? true
}
set {
self.parent?.navigationItem.hidesSearchBarWhenScrolling = newValue
}
}
func navigationBarSizeToFit() {
self.parent?.navigationController?.navigationBar.sizeToFit()
}
}
}
Usage
import SwiftUI
import NavigationSearchBar
struct ContentView: View {
#State var text: String = ""
#State var scopeSelection: Int = 0
var body: some View {
NavigationView {
List {
ForEach(1..<5) { index in
Text("Sample Text")
}
}
.navigationTitle("Navigation")
.navigationSearchBar(text: $text,
scopeSelection: $scopeSelection,
options: [
.automaticallyShowsSearchBar: true,
.obscuresBackgroundDuringPresentation: true,
.hidesNavigationBarDuringPresentation: true,
.hidesSearchBarWhenScrolling: false,
.placeholder: "Search",
.showsBookmarkButton: true,
.scopeButtonTitles: ["All", "Missed", "Other"]
],
actions: [
.onCancelButtonClicked: {
print("Cancel")
},
.onSearchButtonClicked: {
print("Search")
},
.onBookmarkButtonClicked: {
print("Present Bookmarks")
}
], searchResultsContent: {
Text("Search Results for \(text) in \(String(scopeSelection))")
})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I tried making UISearchController work with NavigationView and UIViewRepresentable to inject the search controller but the result was really buggy. Probably the best way to do it is to use a regular UINavigationController instead of NavigationView and then also use a container UIViewController where you set navigationItem.searchController to your UISearchController.

swift show QLPreviewPanel

I want to preview some files using a QLPreviewPanel.
I've included the following ViewController using a Storyboard
class ViewController: NSViewController, QLPreviewPanelDataSource, QLPreviewPanelDelegate {
override func viewDidAppear() {
super.viewDidAppear()
self.nextResponder = MainWindowController.testinstance!.nextResponder
}
#IBAction func btn(_ sender: Any) {
openPreview(url: URL(fileURLWithPath: "/Users/usr/Desktop/test.mp3"))
}
//preview for audio
private var previewURL : URL?
func openPreview(url: URL){
previewURL = url
if let sharedPanel = QLPreviewPanel.shared() {
sharedPanel.delegate = self
sharedPanel.dataSource = self
sharedPanel.makeKeyAndOrderFront(nil)
}
}
func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
return 1
}
func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! {
if previewURL == nil {
return nil
}
return previewURL as? QLPreviewItem
}
override func acceptsPreviewPanelControl(_ panel: QLPreviewPanel!) -> Bool {
return true
}
override func beginPreviewPanelControl(_ panel: QLPreviewPanel!) {
panel.dataSource = self
panel.delegate = self
}
override func endPreviewPanelControl(_ panel: QLPreviewPanel!) {
panel.dataSource = nil
panel.delegate = nil
}}
Everything works fine - but i get the error
[QL] QLError(): -[QLPreviewPanel setDelegate:] called while the panel has no controller - Fix this or this will raise soon.
See comments in QLPreviewPanel.h for -acceptsPreviewPanelControl:/-beginPreviewPanelControl:/-endPreviewPanelControl:.
How do I solve that? Or is it save to just ignore that error?
Declare a QLPreviewController and then try to show items.
Here is an example for viewing and opening files with QLPreviewController
Example of QLPreviewController