I have a macOS SwiftUI app that displays a lot of text fields in a grid. Originally I was using SwiftUI TextField's, but they have the problem that you cannot set the focus order of them (the order they receive focus when you press the tab button). I need to change the focus order from horizontal row-by-row, to vertical column-by-column. There does not appear to be an easy way to do this in SwiftUI.
I found a solution for iOS here, that I tried to modify for mac. The problem is that the delegate functions are not getting called. My current code is below. How do I fix this?
Edit: I've updated my code in light of some of the comments, but the problem still remains the same: no delegate methods are being called.
import SwiftUI
struct OrderedTextField: NSViewRepresentable {
#Binding var text: String
#Binding var selectedField: Int
var tag: Int
func makeNSView(context: NSViewRepresentableContext<OrderedTextField>) -> NSTextField {
let textField = NSTextField()
textField.delegate = context.coordinator
textField.tag = tag
textField.placeholderString = ""
return textField
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
func updateNSView(_ nsView: NSTextField, context: NSViewRepresentableContext<OrderedTextField>) {
context.coordinator.newSelection = { newSelection in
DispatchQueue.main.async {
self.selectedField = newSelection
}
}
if nsView.tag == self.selectedField {
nsView.becomeFirstResponder()
}
}
}
extension OrderedTextField {
class Coordinator: NSObject, NSTextFieldDelegate {
#Binding var text: String
var newSelection: (Int) -> () = { _ in }
init(text: Binding<String>) {
print("Initializing!")
_text = text
}
func textShouldBeginEditing(_ textObject: NSText) -> Bool {
print("Should begin editing!")
return true
}
func textDidBeginEditing(_ notification: Notification) {
print("Began editing")
}
func textDidChange(_ notification: Notification) {
print("textDidChange")
}
func textShouldEndEditing(_ textObject: NSText) -> Bool {
print("should end editing")
return true
}
func textDidEndEditing(_ notification: Notification) {
print("did end editing")
}
}
}
I have the following code taken directly from this blog. This code successfully displays the FirebaseUI with Google sign In, Apple sign In, and Email sign in when I place the SignInViewUI into my view.
Google and Apple sign in both function correctly. Email sign in does nothing when tapped.
import Firebase
import FirebaseUI
import SwiftUI
typealias AM = AuthManager
class AuthManager : NSObject{
static let shared = AuthManager()
var authViewController : UIViewController {
return MyAuthViewController(authUI: FUIAuth.defaultAuthUI()!)
}
init(withNavigationBar : Bool = false){
FirebaseApp.configure()
super.init()
self.setupProviders()
}
private func setupProviders(){
let authUI = FUIAuth.defaultAuthUI()!
let providers: [FUIAuthProvider] = [
FUIGoogleAuth.init(authUI: authUI),
FUIOAuth.appleAuthProvider(),
FUIEmailAuth()]
authUI.providers = providers
}
}
extension AuthManager {
// an optional handler closure for error handling
func signOut(onError handler : ((Error?) -> Void)? = nil ){
do {
try FUIAuth.defaultAuthUI()?.signOut()
if let handler = handler {
handler(nil)
}
}
catch (let err){
if let handler = handler {
handler(err)
}
}
}
func isSignedIn() -> Bool {
if let _ = Firebase.Auth.auth().currentUser{
return true
}
return false
}
}
extension AuthManager {
func setAuthDelegate(_ delegate : FUIAuthDelegate){
FUIAuth.defaultAuthUI()?.delegate = delegate
}
}
class MyAuthViewController : FUIAuthPickerViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scrollView = view.subviews[0]
scrollView.backgroundColor = .clear
let contentView = scrollView.subviews[0]
contentView.backgroundColor = .clear
view.backgroundColor = .clear
}
}
struct SignInViewUI: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<SignInViewUI>) ->
UIViewController {
return AM.shared.authViewController
}
func updateUIViewController(_ uiViewController: UIViewController,
context: UIViewControllerRepresentableContext<SignInViewUI>) {
// empty
}
}
I believe this is something to do with a navigation controller being required for the Email sign in, based on the final comment on this GitHub issue.
Surrounding the SignInViewUI in a NavigationView allows the button to work.
Secondly, the FirebaseApp.configure() line must be removed from the AuthManager and placed into the AppDelegate application(_ application: UIApplication, didFinishLaunchingWithOptions ... method.
I'm having trouble with this Firebase application. I have an enum inside a UIView that shows up as nil even when I set it in viewDidLoad(). The UIView is just a header view, with the enum serving through a switch statement to determine which button to display for which user type. Here's the code for the UIView (shortened to just show necessary code for readability):
enum HeaderViewOptions: Int {
case partner
case client
}
class HeaderView: UIView {
var options: HeaderViewOptions!
//...
override init(frame: CGRect) {
super.init(frame: frame)
//...
switch options {
case .partner:
addSubview(addServiceButton)
//...
case .client:
addSubview(searchButton)
//...
case .none:
break
}
}
}
Here's where the series of functions and variables in order inside the ViewController that set the options variable:
override func viewDidLoad() {
super.viewDidLoad()
checkUser()
}
func checkUser() {
if Auth.auth().currentUser?.uid == nil {
//...
} else {
configure()
}
}
func configure() {
//...
fetchUserData()
//...
}
func fetchUserData() {
guard let currentUid = Auth.auth().currentUser?.uid else { return }
Services.shared.fetchUserData(uid: currentUid) { user in
self.user = user
}
}
private var user: User? {
didSet {
if user?.accountType == .partner {
headerView.options = .partner
} else {
headerView.options = .client
}
}
}
The accountType property for the User object has the same enum options as the HeaderView class.
Update your async function
func fetchUserData() {
guard let currentUid = Auth.auth().currentUser?.uid else { return }
Services.shared.fetchUserData(uid: currentUid) {[weak self] user in
self?.user = user
DispatchQueue.main.async {
self?.configureButtonsInView() // add buttons here
}
}
}
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.
I guess I'm struggling with generics. I want to create simple UIView extension to find recursively a superview of class passed in the function param. I want the function to return optional containing obviously either nil, or object visible as instance of provided class.
extension UIView {
func superviewOfClass<T>(ofClass: T.Type) -> T? {
var currentView: UIView? = self
while currentView != nil {
if currentView is T {
break
} else {
currentView = currentView?.superview
}
}
return currentView as? T
}
}
Any help much appreciated.
Swift 3/4
This is a more concise way:
extension UIView {
func superview<T>(of type: T.Type) -> T? {
return superview as? T ?? superview.compactMap { $0.superview(of: type) }
}
func subview<T>(of type: T.Type) -> T? {
return subviews.compactMap { $0 as? T ?? $0.subview(of: type) }.first
}
}
Usage:
let tableView = someView.superview(of: UITableView.self)
let tableView = someView.subview(of: UITableView.self)
No need to pass in the type of the class you want (at least in Swift 4.1)…
extension UIView {
func firstSubview<T: UIView>() -> T? {
return subviews.compactMap { $0 as? T ?? $0.firstSubview() as? T }.first
}
}
I'm using this.
// Lookup view ancestry for any `UIScrollView`.
if let scrollView = view.searchViewAnchestors(for: UIScrollView.self) {
print("Found scrollView: \(scrollView)")
}
Extension is really a single statement.
extension UIView {
func searchViewAnchestors<ViewType: UIView>(for viewType: ViewType.Type) -> ViewType? {
if let matchingView = self.superview as? ViewType {
return matchingView
} else {
return superview?.searchViewAnchestors(for: viewType)
}
}
}
With this alternative implementation below, you can actually let the call site determine what type to look for, but I found it somewhat unconventional.
extension UIView {
func searchInViewAnchestors<ViewType: UIView>() -> ViewType? {
if let matchingView = self.superview as? ViewType {
return matchingView
} else {
return superview?.searchInViewAnchestors()
}
}
}
You can call it like this.
if let scrollView: UIScrollView = view.searchInViewAnchestors() {
print("Found scrollView: \(scrollView)")
}