带有 SearchController 的 TableView - 未调用 DEINIT

TableView with SearchController - DEINIT not called

我已经从界面生成器向我的应用程序添加了搜索栏和搜索显示控制器。我无法正确地取消初始化 (dealloc)。

它表现出以下行为(swift2,ios9):

如果我在导航控制器中 select "Back" 而不是 select 一个项目,则会发生相同的行为。

code removed - refer to COMPLETE CODE at bottom of post.

感谢任何帮助!

UPDATE 进一步的测试表明,从视图控制器中删除 progressHud/loadingHud 对不调用 DEINIT 完全没有影响。肯定是和tableview或者searchcontroller本身有关...

更新 2 我已经尝试在我的 viewWillDissapear 中调用 searchBarCancelButtonClicked() 方法,但它仍然没有释放。即使您单击 "cancel" 然后导航离开,它也会...

更新 3 将 willDisappear/didDisappear 更改为以下内容对 DEINIT 没有影响 - 但不会出现错误的界面问题(感谢 Polina)。我正在尽我所能获得释放,但到目前为止运气不佳。

    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,并确保闭包正在使用 [weak self] in 并安全地展开它们。 DEINIT 仍然没有被调用。

UPDATE 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 = [=13=]["ident"] {
                        if ident.lowercaseString.rangeOfString(searchText) != nil {
                            return true
                        }
                    }
                    if let name  = [=13=]["name"] {
                        if name.lowercaseString.rangeOfString(searchText) != nil {
                            return true
                        }
                    }
                    if let city = [=13=]["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 = [=14=]["ident"] {
                        if ident.lowercaseString.rangeOfString(searchText) != nil {
                            return true
                        }
                    }
                    if let name  = [=14=]["name"] {
                        if name.lowercaseString.rangeOfString(searchText) != nil {
                            return true
                        }
                    }
                    if let city = [=14=]["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
        }
    }

}

我认为问题在于您在闭包和自我之间创建了强引用循环,因为您在闭包中使用了未包装的弱自我。

例如,这个:

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],您不必将它们视为可选,但老实说,我忘记了无主与弱的优缺点。但我相信,如果您在闭包的捕获列表中声明 weak selfunowned self,而不是在闭包之外并展开它,您的对象将被正确地取消初始化。

删除我的旧答案,找到问题了。

删除:

definesPresentationContext = true // Remove this line...

在这里阅读:

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


UISearchController 一定有错误。某些 Apple 代码必须保留您的控制器。

也许当控制器被取消链接时转换视图(动画)没有完成(它有特殊的toView和fromView等,我们无法到达),这会阻止控制器被释放。

我认为你应该将此作为 Apple 的错误提交。


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

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

这将确保搜索控制器在解除分配时永远不会尝试显示内容,因为这会产生警告和潜在的意外行为。

我 运行 今天也关注这个问题 这行代码似乎可以让你的 class 获得释放

  override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    searchController?.dismissViewControllerAnimated(false, completion: nil)
  }

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