带有SearchController的TableView - DEINIT方法未被调用

6
我已经从界面生成器中添加了一个搜索栏和搜索显示控制器到我的应用程序中。但我无法使其正确地deinit(dealloc)。
它显示以下行为(swift2,ios9):
用户没有搜索任何东西,只是从tableView中选择了一个项目,DEINIT被调用
用户搜索了一些内容(或者只是点击了搜索栏),取消搜索,在tableView中选择项目后,DEINIT被调用
用户搜索了一些内容(或者只是点击了搜索栏),在tableView中选择了一个项目,DEINIT没有被调用 :(
如果我选择导航控制器中的“返回”按钮而不是选择一个项目,则会出现相同的行为。
code removed - refer to COMPLETE CODE at bottom of post.

任何帮助都将不胜感激!
更新:进一步测试表明,从视图控制器完全删除progressHud/loadingHud对DEINIT没有影响。它必须与tableview或searchcontroller本身有关...
更新2:我尝试在my viewWillDissapear中调用searchBarCancelButtonClicked()方法,但它仍然不会释放。即使您点击“取消”然后导航离开也是如此...
更新3:将willDisappear/didDisappear更改为以下内容对DEINIT没有影响,但不会产生错误的界面问题(感谢Polina)。我正在尝试将任何可以的nil为空以获得释放,但到目前为止没有运气。
    override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(true)
    searchBarCancelButtonClicked(searchController.searchBar)
}

override func viewDidDisappear(animated: Bool) {
    print("View did disappear")
    searchController.searchBar.resignFirstResponder()
    searchController.searchBar.endEditing(true)
    searchController.active = false
    loadingHud.removeFromSuperview()
    progressHud.removeFromSuperview()
    searchController.searchBar.delegate = nil
    searchController.searchResultsUpdater = nil
    searchController = nil
    tableView = nil

    super.viewDidDisappear(true)

}

更新4 我还没有找到答案。真的希望有人能帮忙!

更新5 回应@ConfusedByCode - 我已经更新了以下方法,使用[unowned self] in在所有闭包或后台线程操作中:

code removed - refer to COMPLETE CODE at bottom of post

我仍然没有看到DEINIT。我正在核实是否有愚蠢的错误。
更新6:我删除了额外的弱引用,并确保闭包使用[weak self] in并安全地解包它们。DEINIT仍未被调用。
更新7:更改了两个事项都无济于事-将appDel设置为unowned let appDel,并将searchBar.resignFirstResponder()放在finishSearch()中。仍未收到deinit。
完整代码(代表更新7):
对于正确答案,请查看修改后的代码下面粘贴的代码。
class AirportSearchTBVC: UITableViewController, UISearchResultsUpdating, UISearchBarDelegate {
var airportData = [Dictionary<String, String>]()
var filteredData = [Dictionary<String, String>]()
var searchController: UISearchController!
unowned let appDel = UIApplication.sharedApplication().delegate as! AppDelegate
var progressHud: ProgressHUD!
var loadingHud: ProgressHUD!
var arrDepOfFlight: String!
var dateOfFlight: NSDate!
var tailNum: String!
var selectedAirportIdent: String!

deinit {
    print("TBVC Dealloc")

}
override func viewDidLoad() {
    super.viewDidLoad()
}

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(true)

    progressHud = ProgressHUD(text: "Searching")
    loadingHud = ProgressHUD(text: "Loading")
    searchController = UISearchController(searchResultsController: nil)
    searchController.searchResultsUpdater = self
    searchController.dimsBackgroundDuringPresentation = false
    searchController.searchBar.sizeToFit()
    tableView.tableHeaderView = searchController.searchBar
    definesPresentationContext = true
    searchController.hidesNavigationBarDuringPresentation = false
    searchController.searchBar.delegate = self
    view.addSubview(loadingHud)

    appDel.backgroundThread(background: { [weak self] in
        if let weakSelf = self {
            let airportHelper = AirportHelper()
            weakSelf.airportData = airportHelper.getAirportSearchData()
        }
        },
        completion: {
            dispatch_async(dispatch_get_main_queue()) { [weak self] in
                if let weakSelf = self {
                    if weakSelf.isVisible && weakSelf.isTopViewController {
                        weakSelf.filteredData = (weakSelf.airportData)
                        weakSelf.loadingHud.removeFromSuperview()
                        weakSelf.updateSearchResultsForSearchController(weakSelf.searchController)
                        weakSelf.tableView.reloadData()

                    }

                }
            }
    });

}

//MARK: Searchbar methods (All background thread methods are in here)
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
    searchController.searchBar.endEditing(true)
    searchController.searchBar.resignFirstResponder()
    filteredData = airportData
    tableView.reloadData()
}
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
    if isVisible && isTopViewController {
        if let startCount = searchController.searchBar.text?.length {
            if searchController.searchBar.text!.length >= 3 && searchController.searchBar.text!.length == startCount{
                view.addSubview(progressHud)
                finishSearch()
            }
        }
    }
}
func finishSearch () {
    appDel.backgroundThread(background: { [weak self] in
        if let weakSelf = self {
            if weakSelf.isVisible && weakSelf.isTopViewController {
                let searchText = weakSelf.searchController.searchBar.text!.lowercaseString

                weakSelf.searchController.searchBar.resignFirstResponder()
                weakSelf.filteredData = weakSelf.airportData.filter{
                    if let ident = $0["ident"] {
                        if ident.lowercaseString.rangeOfString(searchText) != nil {
                            return true
                        }
                    }
                    if let name  = $0["name"] {
                        if name.lowercaseString.rangeOfString(searchText) != nil {
                            return true
                        }
                    }
                    if let city = $0["municipality"] {
                        if city.lowercaseString.rangeOfString(searchText) != nil {
                            return true
                        }
                    }
                    return false
                }
            }
        }
        },
        completion: {
            dispatch_async(dispatch_get_main_queue()) { [weak self] in

                if let weakSelf = self {
                    if weakSelf.isVisible && weakSelf.isTopViewController {
                        weakSelf.tableView.reloadData()
                        weakSelf.progressHud.removeFromSuperview()
                    }
                }
            }
    });
}

func updateSearchResultsForSearchController(searchController: UISearchController) {
    if isVisible && isTopViewController {
        if let startCount = searchController.searchBar.text?.length {
            if searchController.searchBar.text!.length >= 3 && searchController.searchBar.text!.length == startCount{
                view.addSubview(progressHud)
                finishSearch()
            }
        }
    }
}


//MARK: Table view methods:
override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return 72
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    // #warning Incomplete implementation, return the number of sections
    return 1
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // #warning Incomplete implementation, return the number of rows



    if searchController.active {
        return filteredData.count
    } else {
        return airportData.count
    }


}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    var cell: AirportSearchTableViewCell
    cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! AirportSearchTableViewCell
    if searchController.active {
        let airportDict = filteredData[indexPath.row]
        let airportIdent = airportDict["ident"]
        let airportName = airportDict["name"]
        let airportRegion = airportDict["iso_region"]
        let airportCity = airportDict["municipality"]
        cell.loadItem(airportIdent, name: airportName, region: airportRegion, city: airportCity)
    } else {
        let airportDict = airportData[indexPath.row]
        let airportIdent = airportDict["ident"]
        let airportName = airportDict["name"]
        let airportRegion = airportDict["iso_region"]
        let airportCity = airportDict["municipality"]
        cell.loadItem(airportIdent, name: airportName, region: airportRegion, city: airportCity)
    }
    return cell
}
override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(true)
    searchBarCancelButtonClicked(searchController.searchBar)
}

override func viewDidDisappear(animated: Bool) {
    print("View did disappear")
    searchController.searchBar.resignFirstResponder()
    searchController.searchBar.endEditing(true)
    searchController.active = false
    searchController.delegate = nil
    searchController.resignFirstResponder()
    loadingHud.removeFromSuperview()
    progressHud.removeFromSuperview()
    searchController.searchBar.delegate = nil
    searchController.searchResultsUpdater = nil
    searchController.removeFromParentViewController()
    searchController = nil
    tableView = nil
    super.viewDidDisappear(true)

}

func delay(delay:Double, closure:()->()) {
    dispatch_after(
        dispatch_time(
            DISPATCH_TIME_NOW,
            Int64(delay * Double(NSEC_PER_SEC))
        ),
        dispatch_get_main_queue(), closure)
}



// MARK: - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let cell = tableView.cellForRowAtIndexPath(indexPath) as! AirportSearchTableViewCell
    selectedAirportIdent = cell.identLbl.text!
    self.performSegueWithIdentifier("searchMapVC", sender: nil)
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    // Get the new view controller using segue.destinationViewController.
    // Pass the selected object to the new view controller.
    if segue.identifier == "searchMapVC" {
        let mapVC = segue.destinationViewController as! SearchMapController
        mapVC.arrDepOfFlight = arrDepOfFlight
        mapVC.dateOfFlight = dateOfFlight
        mapVC.tailNum = tailNum
        mapVC.selectedAirportIdent = selectedAirportIdent
    }
}
}
//MARK: EXTENSIONS
extension String {
    var length: Int { return characters.count    }  // Swift 2.0
}
extension UIViewController {
    public var isVisible: Bool {
        if isViewLoaded() {
            return view.window != nil
        }
        return false
}

public var isTopViewController: Bool {
        if self.navigationController != nil {
            return self.navigationController?.visibleViewController === self
        } else if self.tabBarController != nil {
            return self.tabBarController?.selectedViewController == self && self.presentedViewController == nil
        } else {
            return self.presentedViewController == nil && self.isVisible
        }
    }

}

修正后的代码[已修复] 如Mikael Hellman所建议的,definesPresentationContext存在某种保留错误,最初在我的viewWillAppear方法中。我已经删除了那行代码并对我的代码进行了一些微调。现在它完美地工作了。

非常感谢您的努力和答案!同时,也要感谢@confusedByCode的帮助——我相信他的建议也是我的问题的一个组成部分,但最终没有成为最终答案。

import UIKit

class AirportSearchTBVC: UITableViewController, UISearchResultsUpdating, UISearchBarDelegate {
var airportData = [Dictionary<String, String>]()
var filteredData = [Dictionary<String, String>]()
var searchController: UISearchController!
let appDel = UIApplication.sharedApplication().delegate as! AppDelegate
var progressHud: ProgressHUD!
var loadingHud: ProgressHUD!
var arrDepOfFlight: String!
var dateOfFlight: NSDate!
var tailNum: String!
var selectedAirportIdent: String!

deinit {
    print("TBVC Dealloc")

}
override func viewDidLoad() {
    super.viewDidLoad()
}

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(true)

    progressHud = ProgressHUD(text: "Searching")
    loadingHud = ProgressHUD(text: "Loading")
    searchController = UISearchController(searchResultsController: nil)
    searchController.searchResultsUpdater = self
    searchController.dimsBackgroundDuringPresentation = false
    searchController.searchBar.sizeToFit()
    tableView.tableHeaderView = searchController.searchBar
    //definesPresentationContext = true
    searchController.hidesNavigationBarDuringPresentation = false
    searchController.searchBar.delegate = self
    view.addSubview(loadingHud)

    appDel.backgroundThread(background: { [weak self] in
        if let weakSelf = self {
            let airportHelper = AirportHelper()
            weakSelf.airportData = airportHelper.getAirportSearchData()
        }
        },
        completion: {
            dispatch_async(dispatch_get_main_queue()) { [weak self] in
                if let weakSelf = self {
                    print("isVisible: \(weakSelf.isVisible)")
                    print("isTopViewController: \(weakSelf.isTopViewController)")
                    if weakSelf.isVisible  {
                        weakSelf.filteredData = (weakSelf.airportData)
                        weakSelf.loadingHud.removeFromSuperview()
                        weakSelf.updateSearchResultsForSearchController(weakSelf.searchController)
                        weakSelf.tableView.reloadData()
                    }


                }
            }
    });

}

//MARK: Searchbar methods (All background thread methods are in here)
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
    searchController.searchBar.endEditing(true)
    searchController.searchBar.resignFirstResponder()
    filteredData = airportData
    tableView.reloadData()
}
func searchBarSearchButtonClicked(searchBar: UISearchBar) {

        if let startCount = searchController.searchBar.text?.length {
            if searchController.searchBar.text!.length >= 3 && searchController.searchBar.text!.length == startCount{
                view.addSubview(progressHud)
                finishSearch()
            }
        }

}
func finishSearch () {
    appDel.backgroundThread(background: { [weak self] in
        if let weakSelf = self {

                let searchText = weakSelf.searchController.searchBar.text!.lowercaseString

                //weakSelf.searchController.searchBar.resignFirstResponder()
                weakSelf.filteredData = weakSelf.airportData.filter{
                    if let ident = $0["ident"] {
                        if ident.lowercaseString.rangeOfString(searchText) != nil {
                            return true
                        }
                    }
                    if let name  = $0["name"] {
                        if name.lowercaseString.rangeOfString(searchText) != nil {
                            return true
                        }
                    }
                    if let city = $0["municipality"] {
                        if city.lowercaseString.rangeOfString(searchText) != nil {
                            return true
                        }
                    }
                    return false
                }

        }
        },
        completion: {
            dispatch_async(dispatch_get_main_queue()) { [unowned self] in

                if self.isVisible {

                        self.tableView.reloadData()
                        self.progressHud.removeFromSuperview()
                }



            }
    });
}

func updateSearchResultsForSearchController(searchController: UISearchController) {

        if let startCount = searchController.searchBar.text?.length {
            if searchController.searchBar.text!.length >= 3 && searchController.searchBar.text!.length == startCount{
                view.addSubview(progressHud)
                finishSearch()
            }
        }

}


//MARK: Table view methods:
override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return 72
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    // #warning Incomplete implementation, return the number of sections
    return 1
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // #warning Incomplete implementation, return the number of rows



    if searchController.active {
        return filteredData.count
    } else {
        return airportData.count
    }


}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    var cell: AirportSearchTableViewCell
    cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! AirportSearchTableViewCell
    if searchController.active {
        let airportDict = filteredData[indexPath.row]
        let airportIdent = airportDict["ident"]
        let airportName = airportDict["name"]
        let airportRegion = airportDict["iso_region"]
        let airportCity = airportDict["municipality"]
        cell.loadItem(airportIdent, name: airportName, region: airportRegion, city: airportCity)
    } else {
        let airportDict = airportData[indexPath.row]
        let airportIdent = airportDict["ident"]
        let airportName = airportDict["name"]
        let airportRegion = airportDict["iso_region"]
        let airportCity = airportDict["municipality"]
        cell.loadItem(airportIdent, name: airportName, region: airportRegion, city: airportCity)
    }
    return cell
}
override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(true)
    searchController.active = false
    loadingHud.removeFromSuperview()
    progressHud.removeFromSuperview()
}

override func viewDidDisappear(animated: Bool) {
    print("View did disappear")


    super.viewDidDisappear(true)

}

func delay(delay:Double, closure:()->()) {
    dispatch_after(
        dispatch_time(
            DISPATCH_TIME_NOW,
            Int64(delay * Double(NSEC_PER_SEC))
        ),
        dispatch_get_main_queue(), closure)
}



// MARK: - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let cell = tableView.cellForRowAtIndexPath(indexPath) as! AirportSearchTableViewCell
    selectedAirportIdent = cell.identLbl.text!
    self.performSegueWithIdentifier("searchMapVC", sender: nil)
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    // Get the new view controller using segue.destinationViewController.
    // Pass the selected object to the new view controller.
    if segue.identifier == "searchMapVC" {
        let mapVC = segue.destinationViewController as! SearchMapController
        mapVC.arrDepOfFlight = arrDepOfFlight
        mapVC.dateOfFlight = dateOfFlight
        mapVC.tailNum = tailNum
        mapVC.selectedAirportIdent = selectedAirportIdent
    }
}
}
//MARK: EXTENSIONS
extension String {
    var length: Int { return characters.count    }  // Swift 2.0
}
extension UIViewController {
    public var isVisible: Bool {
        if isViewLoaded() {
            return view.window != nil
        }
        return false
}

public var isTopViewController: Bool {
        if self.navigationController != nil {
            return self.navigationController?.visibleViewController === self
        } else if self.tabBarController != nil {
            return self.tabBarController?.selectedViewController == self && self.presentedViewController == nil
        } else {
            return self.presentedViewController == nil && self.isVisible
        }
    }

}

我已经找到了错误的位置,但离解决方案还很远...当你集中搜索时,deinit不会被触发。我认为这是因为有一些苹果魔法自动触发了动画(跟随superviews,你会发现一个由苹果拥有的UITransitionView)。我相信正是这个转换保留了控制器...但我没有找到任何方法来将其置空,或者防止这个转换发生.... :( - Mikael Hellman
至少你靠近了!我肯定没有——我感觉我尝试了所有的东西,而且这让我非常沮丧 :( - Charlie
3个回答

7
我今天也遇到了这个问题。这行代码似乎可以使你的类被释放。
  override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    searchController?.dismissViewControllerAnimated(false, completion: nil)
  }

这是我在dropbox上的示例项目: https://www.dropbox.com/s/zzs0m4n9maxd2u5/TestSearch.zip?dl=0

不再需要将 definesPresentationContext 设置为 false。这是正确的处理方式。 - Asad Ali

6

我删除了之前的答案,找到了问题所在。

请移除:

definesPresentationContext = true // Remove this line...

请点击以下链接查看更多有关iOS开发者文档中UIViewController类的信息:

https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/#//apple_ref/occ/instp/UIViewController/definesPresentationContext


在UISearchController中可能存在一个bug,某些苹果代码可能会保留您的控制器。

也许当控制器被取消链接时(因为它具有特殊的toView和fromView等),转换视图(动画)尚未完成,这会阻止控制器被释放。

我建议您向苹果报告此问题。


此外,我建议将您的deinit更改为:

deinit {
  print("TBVC Dealloc")
  if let superView = searchController.view.superview
  {
    superView.removeFromSuperview()
  }
}

这样可以确保搜索控制器在被释放时不会尝试呈现任何内容,因为这会产生警告和潜在的意外行为。


1
这真的很令人兴奋 - 我会尽快尝试并回报! - Charlie
您,先生,真是个牛人。我百万年也不可能找到那个问题。非常感谢您的努力,希望您享受这份奖赏! - Charlie
@Charlie 謝謝!很高興能幫上忙。 - Mikael Hellman
5
根据搜索栏的上下文环境,有时需要将definesPresentationContext设置为true。例如,如果您的UITableView被推到UINavigationController上,则通常希望将definesPresentationContext设置为true。否则,搜索栏会从导航控制器中呈现,并在推送和弹出控制器时引起问题。因此,尽管这似乎解决了问题,但它并不能处理每个使用情况。 - chris
1
@chris是正确的。对于任何卡住的人来说,除了将definesPresentationContext设置为true之外,您还需要在viewWillDisappear中隐藏searchBar,并在viewWillAppear中使其重新出现。这不会将searchBar添加到其他视图控制器中。 - Ameya Vichare

0

我认为问题在于你在闭包和self之间创建了强引用循环,因为你在闭包中使用了一个非可选的weak self。

比如说,这段代码:

if let safeSelf = weakSelf {
    appDel.backgroundThread(background: {
            let airportHelper = AirportHelper()
            safeSelf.airportData = airportHelper.getAirportSearchData()
        },
        completion: {
            dispatch_async(dispatch_get_main_queue()) {
                if safeSelf.isVisible && safeSelf.isTopViewController {
                    safeSelf.filteredData = (safeSelf.airportData)
                    safeSelf.loadingHud.removeFromSuperview()
                    safeSelf.tableView.reloadData()
                }
            }
    });
}

应该是:
appDel.backgroundThread(background: { [weak self] in
        let airportHelper = AirportHelper()
        self?.airportData = airportHelper.getAirportSearchData()
    },
    completion: {
        dispatch_async(dispatch_get_main_queue()) { [weak self] in
            if self?.isVisible && safeSelf.isTopViewController {
                self?.filteredData = (safeSelf.airportData)
                self?.loadingHud.removeFromSuperview()
                self?.tableView.reloadData()
            }
        }
});

你也可以使用[unowned self],这样就不必将它们视为可选项,但说实话,我忘记了unowned和weak的优缺点。但我相信,如果你在闭包中的捕获列表中声明weak selfunowned self,而不是在闭包外部并解包它,你的对象将被正确释放。


我现在正在尝试使用[unowned self],以避免一些令人困惑的解包错误,我担心这可能会导致保留。感谢您的帖子,我会报告发生了什么! - Charlie
我已经在我的问题中添加了更新5,您能否请审核更新并指出我做错了什么?我尝试应用您的答案,但结果是负面的。 - Charlie
我刚刚注意到当你将searchController和tableView设置为nil时,你能够deinit vc。这可能是因为你为weak self创建了一个局部变量,并将其设置为resultsUpdater和searchController的代理。文档中说searchControllers会自动将searchResultsUpdater设置为弱引用,所以你不需要自己设置。尝试摆脱所有额外的weak self,并直接将searchResultsUpdater和代理设置为self。你只需要在闭包的捕获列表中使用weak self。我认为它们可能会带来更多的麻烦而不是好处。 - ConfusedByCode
我相信我已经删除了所有有问题的weak self附加项。请查看更新6-仍未解除分配。我尝试简化使用后台线程的方法,以使其更容易。我真的非常感激您的帮助,超出您的想象! - Charlie
哇,我恐怕没有更多的想法了。我最后注意到的是,你可以尝试在searchBarSearchButtonClickedupdateSearchResultsForSeachController中调用searchBar.resignFirstReponder()。如果这不起作用,你可能需要在finishSearch方法的一个闭包中调用resignFirstResponder,因为它是异步执行的。除此之外,我已经没有更多的想法了,抱歉! - ConfusedByCode
我认为我已经按照你所描述的做了,但似乎没有起作用:(非常感谢你迄今为止的帮助 - 没有这些想法我不知道该怎么做。不过我不确定是否错过了你认为我应该更改的任何地方。 - Charlie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接